mirror of https://github.com/zulip/zulip.git
user_profile: Remove 'tutorial_status' field.
The 'tutorial_status' field on 'UserProfile' model is no longer used to show onboarding tutorial. This commit removes the 'tutorial_status' field, 'POST users/me/tutorial_status' endpoint, and 'needs_tutorial' parameter in 'page_params'. Fixes part of zulip#30043.
This commit is contained in:
parent
ee806c49b9
commit
52a9846cdf
|
@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 10.0
|
## Changes in Zulip 10.0
|
||||||
|
|
||||||
|
**Feature level 282**
|
||||||
|
|
||||||
|
* `POST users/me/tutorial_status`: Removed this undocumented endpoint,
|
||||||
|
as the state that it maintained has been replaced by a cleaner
|
||||||
|
`onboarding_steps` implementation.
|
||||||
|
|
||||||
**Feature level 281**
|
**Feature level 281**
|
||||||
|
|
||||||
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue):
|
* [`GET /events`](/api/get-events), [`POST /register`](/api/register-queue):
|
||||||
|
|
|
@ -34,7 +34,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
|
||||||
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
# new level means in api_docs/changelog.md, as well as "**Changes**"
|
||||||
# entries in the endpoint's documentation in `zulip.yaml`.
|
# entries in the endpoint's documentation in `zulip.yaml`.
|
||||||
|
|
||||||
API_FEATURE_LEVEL = 281 # Last bumped for realm_can_delete_any_message_group
|
API_FEATURE_LEVEL = 282 # Last bumped for removing "POST users/me/tutorial_status"
|
||||||
|
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||||
|
|
|
@ -43,7 +43,6 @@ const home_params_schema = default_params_schema
|
||||||
narrow: z.optional(z.array(narrow_term_schema)),
|
narrow: z.optional(z.array(narrow_term_schema)),
|
||||||
narrow_stream: z.optional(z.string()),
|
narrow_stream: z.optional(z.string()),
|
||||||
narrow_topic: z.optional(z.string()),
|
narrow_topic: z.optional(z.string()),
|
||||||
needs_tutorial: z.boolean(),
|
|
||||||
promote_sponsoring_zulip: z.boolean(),
|
promote_sponsoring_zulip: z.boolean(),
|
||||||
// `realm_rendered_description` is only sent for spectators, because
|
// `realm_rendered_description` is only sent for spectators, because
|
||||||
// it isn't displayed for logged-in users and requires markdown
|
// it isn't displayed for logged-in users and requires markdown
|
||||||
|
|
|
@ -2,27 +2,18 @@ import $ from "jquery";
|
||||||
|
|
||||||
import * as blueslip from "./blueslip";
|
import * as blueslip from "./blueslip";
|
||||||
import * as loading from "./loading";
|
import * as loading from "./loading";
|
||||||
import {page_params} from "./page_params";
|
|
||||||
import * as util from "./util";
|
|
||||||
|
|
||||||
export let page_load_time: number | undefined;
|
export let page_load_time: number | undefined;
|
||||||
|
|
||||||
// Miscellaneous early setup.
|
// Miscellaneous early setup.
|
||||||
$(() => {
|
$(() => {
|
||||||
if (util.is_mobile()) {
|
|
||||||
// Disable the tutorial; it's ugly on mobile.
|
|
||||||
page_params.needs_tutorial = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
page_load_time = Date.now();
|
page_load_time = Date.now();
|
||||||
|
|
||||||
// Display loading indicator. This disappears after the first
|
// Display loading indicator. This disappears after the first
|
||||||
// get_events completes.
|
// get_events completes.
|
||||||
if (!page_params.needs_tutorial) {
|
|
||||||
loading.make_indicator($("#page_loading_indicator"), {
|
loading.make_indicator($("#page_loading_indicator"), {
|
||||||
abs_positioned: true,
|
abs_positioned: true,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
$.fn.get_offset_to_window = function () {
|
$.fn.get_offset_to_window = function () {
|
||||||
return this[0]!.getBoundingClientRect();
|
return this[0]!.getBoundingClientRect();
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
import * as channel from "./channel";
|
|
||||||
import {page_params} from "./page_params";
|
|
||||||
|
|
||||||
function set_tutorial_status(status, callback) {
|
|
||||||
return channel.post({
|
|
||||||
url: "/json/users/me/tutorial_status",
|
|
||||||
data: {status},
|
|
||||||
success: callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initialize() {
|
|
||||||
if (page_params.needs_tutorial) {
|
|
||||||
set_tutorial_status("started");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -132,7 +132,6 @@ import * as tippyjs from "./tippyjs";
|
||||||
import * as topic_list from "./topic_list";
|
import * as topic_list from "./topic_list";
|
||||||
import * as topic_popover from "./topic_popover";
|
import * as topic_popover from "./topic_popover";
|
||||||
import * as transmit from "./transmit";
|
import * as transmit from "./transmit";
|
||||||
import * as tutorial from "./tutorial";
|
|
||||||
import * as typeahead_helper from "./typeahead_helper";
|
import * as typeahead_helper from "./typeahead_helper";
|
||||||
import * as typing from "./typing";
|
import * as typing from "./typing";
|
||||||
import * as unread from "./unread";
|
import * as unread from "./unread";
|
||||||
|
@ -642,9 +641,6 @@ export function initialize_everything(state_data) {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// This needs to happen after activity_ui.initialize, so that user_filter
|
|
||||||
// is defined. Also, must happen after people.initialize()
|
|
||||||
tutorial.initialize();
|
|
||||||
|
|
||||||
// All overlays, and also activity_ui, must be initialized before hashchange.js
|
// All overlays, and also activity_ui, must be initialized before hashchange.js
|
||||||
hashchange.initialize();
|
hashchange.initialize();
|
||||||
|
|
|
@ -61,7 +61,6 @@ def bulk_create_users(
|
||||||
tos_version,
|
tos_version,
|
||||||
timezone,
|
timezone,
|
||||||
default_language=realm.default_language,
|
default_language=realm.default_language,
|
||||||
tutorial_status=UserProfile.TUTORIAL_FINISHED,
|
|
||||||
email_address_visibility=email_address_visibility,
|
email_address_visibility=email_address_visibility,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,6 @@ def create_user_profile(
|
||||||
tos_version: str | None,
|
tos_version: str | None,
|
||||||
timezone: str,
|
timezone: str,
|
||||||
default_language: str,
|
default_language: str,
|
||||||
tutorial_status: str = UserProfile.TUTORIAL_WAITING,
|
|
||||||
force_id: int | None = None,
|
force_id: int | None = None,
|
||||||
force_date_joined: datetime | None = None,
|
force_date_joined: datetime | None = None,
|
||||||
*,
|
*,
|
||||||
|
@ -122,7 +121,6 @@ def create_user_profile(
|
||||||
is_mirror_dummy=is_mirror_dummy,
|
is_mirror_dummy=is_mirror_dummy,
|
||||||
tos_version=tos_version,
|
tos_version=tos_version,
|
||||||
timezone=timezone,
|
timezone=timezone,
|
||||||
tutorial_status=tutorial_status,
|
|
||||||
default_language=default_language,
|
default_language=default_language,
|
||||||
delivery_email=email,
|
delivery_email=email,
|
||||||
email_address_visibility=email_address_visibility,
|
email_address_visibility=email_address_visibility,
|
||||||
|
|
|
@ -140,7 +140,6 @@ def build_page_params_for_home_page_load(
|
||||||
narrow: list[NarrowTerm],
|
narrow: list[NarrowTerm],
|
||||||
narrow_stream: Stream | None,
|
narrow_stream: Stream | None,
|
||||||
narrow_topic_name: str | None,
|
narrow_topic_name: str | None,
|
||||||
needs_tutorial: bool,
|
|
||||||
) -> tuple[int, dict[str, object]]:
|
) -> tuple[int, dict[str, object]]:
|
||||||
"""
|
"""
|
||||||
This function computes page_params for when we load the home page.
|
This function computes page_params for when we load the home page.
|
||||||
|
@ -211,7 +210,6 @@ def build_page_params_for_home_page_load(
|
||||||
corporate_enabled=settings.CORPORATE_ENABLED,
|
corporate_enabled=settings.CORPORATE_ENABLED,
|
||||||
## Misc. extra data.
|
## Misc. extra data.
|
||||||
language_list=get_language_list(),
|
language_list=get_language_list(),
|
||||||
needs_tutorial=needs_tutorial,
|
|
||||||
furthest_read_time=furthest_read_time,
|
furthest_read_time=furthest_read_time,
|
||||||
bot_types=get_bot_types(user_profile),
|
bot_types=get_bot_types(user_profile),
|
||||||
two_fa_enabled=two_fa_enabled,
|
two_fa_enabled=two_fa_enabled,
|
||||||
|
|
|
@ -85,8 +85,3 @@ def copy_onboarding_steps(source_profile: UserProfile, target_profile: UserProfi
|
||||||
onboarding_step=onboarding_step.onboarding_step,
|
onboarding_step=onboarding_step.onboarding_step,
|
||||||
timestamp=onboarding_step.timestamp,
|
timestamp=onboarding_step.timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: The 'tutorial_status' field of 'UserProfile' model
|
|
||||||
# is no longer used. Remove it.
|
|
||||||
target_profile.tutorial_status = source_profile.tutorial_status
|
|
||||||
target_profile.save(update_fields=["tutorial_status"])
|
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Generated by Django 5.0.6 on 2024-07-25 06:59
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("zerver", "0568_mark_narrow_to_dm_with_welcome_bot_new_user_as_read"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="userprofile",
|
||||||
|
name="tutorial_status",
|
||||||
|
),
|
||||||
|
]
|
|
@ -591,22 +591,6 @@ class UserProfile(AbstractBaseUser, PermissionsMixin, UserBaseSettings):
|
||||||
# us, pre-thumbnailing.
|
# us, pre-thumbnailing.
|
||||||
avatar_hash = models.CharField(null=True, max_length=64)
|
avatar_hash = models.CharField(null=True, max_length=64)
|
||||||
|
|
||||||
# TODO: TUTORIAL_STATUS was originally an optimization designed to
|
|
||||||
# allow us to skip querying the OnboardingStep table when loading
|
|
||||||
# /. This optimization is no longer effective, so it's possible we
|
|
||||||
# should delete it.
|
|
||||||
TUTORIAL_WAITING = "W"
|
|
||||||
TUTORIAL_STARTED = "S"
|
|
||||||
TUTORIAL_FINISHED = "F"
|
|
||||||
TUTORIAL_STATES = (
|
|
||||||
(TUTORIAL_WAITING, "Waiting"),
|
|
||||||
(TUTORIAL_STARTED, "Started"),
|
|
||||||
(TUTORIAL_FINISHED, "Finished"),
|
|
||||||
)
|
|
||||||
tutorial_status = models.CharField(
|
|
||||||
default=TUTORIAL_WAITING, choices=TUTORIAL_STATES, max_length=1
|
|
||||||
)
|
|
||||||
|
|
||||||
zoom_token = models.JSONField(default=None, null=True)
|
zoom_token = models.JSONField(default=None, null=True)
|
||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
|
@ -1116,7 +1116,6 @@ class TestHumanUsersOnlyDecorator(ZulipTestCase):
|
||||||
"/api/v1/users/me/android_gcm_reg_id",
|
"/api/v1/users/me/android_gcm_reg_id",
|
||||||
"/api/v1/users/me/onboarding_steps",
|
"/api/v1/users/me/onboarding_steps",
|
||||||
"/api/v1/users/me/presence",
|
"/api/v1/users/me/presence",
|
||||||
"/api/v1/users/me/tutorial_status",
|
|
||||||
]
|
]
|
||||||
for endpoint in post_endpoints:
|
for endpoint in post_endpoints:
|
||||||
result = self.api_post(default_bot, endpoint)
|
result = self.api_post(default_bot, endpoint)
|
||||||
|
|
|
@ -55,7 +55,6 @@ class HomeTest(ZulipTestCase):
|
||||||
"login_page",
|
"login_page",
|
||||||
"narrow",
|
"narrow",
|
||||||
"narrow_stream",
|
"narrow_stream",
|
||||||
"needs_tutorial",
|
|
||||||
"no_event_queue",
|
"no_event_queue",
|
||||||
"page_type",
|
"page_type",
|
||||||
"promote_sponsoring_zulip",
|
"promote_sponsoring_zulip",
|
||||||
|
@ -361,7 +360,6 @@ class HomeTest(ZulipTestCase):
|
||||||
"language_cookie_name",
|
"language_cookie_name",
|
||||||
"language_list",
|
"language_list",
|
||||||
"login_page",
|
"login_page",
|
||||||
"needs_tutorial",
|
|
||||||
"no_event_queue",
|
"no_event_queue",
|
||||||
"page_type",
|
"page_type",
|
||||||
"promote_sponsoring_zulip",
|
"promote_sponsoring_zulip",
|
||||||
|
|
|
@ -2823,7 +2823,6 @@ class UserSignUpTest(ZulipTestCase):
|
||||||
hamlet_in_zulip.emojiset = "twitter"
|
hamlet_in_zulip.emojiset = "twitter"
|
||||||
hamlet_in_zulip.high_contrast_mode = True
|
hamlet_in_zulip.high_contrast_mode = True
|
||||||
hamlet_in_zulip.enter_sends = True
|
hamlet_in_zulip.enter_sends = True
|
||||||
hamlet_in_zulip.tutorial_status = UserProfile.TUTORIAL_FINISHED
|
|
||||||
hamlet_in_zulip.email_address_visibility = UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE
|
hamlet_in_zulip.email_address_visibility = UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE
|
||||||
hamlet_in_zulip.save()
|
hamlet_in_zulip.save()
|
||||||
|
|
||||||
|
@ -2845,7 +2844,6 @@ class UserSignUpTest(ZulipTestCase):
|
||||||
self.assertEqual(hamlet.high_contrast_mode, False)
|
self.assertEqual(hamlet.high_contrast_mode, False)
|
||||||
self.assertEqual(hamlet.enable_stream_audible_notifications, False)
|
self.assertEqual(hamlet.enable_stream_audible_notifications, False)
|
||||||
self.assertEqual(hamlet.enter_sends, False)
|
self.assertEqual(hamlet.enter_sends, False)
|
||||||
self.assertEqual(hamlet.tutorial_status, UserProfile.TUTORIAL_WAITING)
|
|
||||||
|
|
||||||
def test_signup_with_user_settings_from_another_realm(self) -> None:
|
def test_signup_with_user_settings_from_another_realm(self) -> None:
|
||||||
hamlet_in_zulip = self.example_user("hamlet")
|
hamlet_in_zulip = self.example_user("hamlet")
|
||||||
|
@ -2863,7 +2861,6 @@ class UserSignUpTest(ZulipTestCase):
|
||||||
hamlet_in_zulip.emojiset = "twitter"
|
hamlet_in_zulip.emojiset = "twitter"
|
||||||
hamlet_in_zulip.high_contrast_mode = True
|
hamlet_in_zulip.high_contrast_mode = True
|
||||||
hamlet_in_zulip.enter_sends = True
|
hamlet_in_zulip.enter_sends = True
|
||||||
hamlet_in_zulip.tutorial_status = UserProfile.TUTORIAL_FINISHED
|
|
||||||
hamlet_in_zulip.email_address_visibility = UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE
|
hamlet_in_zulip.email_address_visibility = UserProfile.EMAIL_ADDRESS_VISIBILITY_EVERYONE
|
||||||
hamlet_in_zulip.save()
|
hamlet_in_zulip.save()
|
||||||
|
|
||||||
|
@ -2909,7 +2906,6 @@ class UserSignUpTest(ZulipTestCase):
|
||||||
self.assertEqual(hamlet_in_lear.high_contrast_mode, True)
|
self.assertEqual(hamlet_in_lear.high_contrast_mode, True)
|
||||||
self.assertEqual(hamlet_in_lear.enter_sends, True)
|
self.assertEqual(hamlet_in_lear.enter_sends, True)
|
||||||
self.assertEqual(hamlet_in_lear.enable_stream_audible_notifications, False)
|
self.assertEqual(hamlet_in_lear.enable_stream_audible_notifications, False)
|
||||||
self.assertEqual(hamlet_in_lear.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
hamlet_in_lear.email_address_visibility, UserProfile.EMAIL_ADDRESS_VISIBILITY_NOBODY
|
hamlet_in_lear.email_address_visibility, UserProfile.EMAIL_ADDRESS_VISIBILITY_NOBODY
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,7 +4,6 @@ from typing_extensions import override
|
||||||
from zerver.actions.message_send import internal_send_private_message
|
from zerver.actions.message_send import internal_send_private_message
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import message_stream_count, most_recent_message
|
from zerver.lib.test_helpers import message_stream_count, most_recent_message
|
||||||
from zerver.models import UserProfile
|
|
||||||
from zerver.models.users import get_system_bot
|
from zerver.models.users import get_system_bot
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,21 +27,6 @@ class TutorialTests(ZulipTestCase):
|
||||||
disable_external_notifications=True,
|
disable_external_notifications=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tutorial_status(self) -> None:
|
|
||||||
user = self.example_user("hamlet")
|
|
||||||
self.login_user(user)
|
|
||||||
|
|
||||||
cases = [
|
|
||||||
("started", UserProfile.TUTORIAL_STARTED),
|
|
||||||
("finished", UserProfile.TUTORIAL_FINISHED),
|
|
||||||
]
|
|
||||||
for incoming_status, expected_db_status in cases:
|
|
||||||
params = dict(status=incoming_status)
|
|
||||||
result = self.client_post("/json/users/me/tutorial_status", params)
|
|
||||||
self.assert_json_success(result)
|
|
||||||
user = self.example_user("hamlet")
|
|
||||||
self.assertEqual(user.tutorial_status, expected_db_status)
|
|
||||||
|
|
||||||
def test_response_to_pm_for_app(self) -> None:
|
def test_response_to_pm_for_app(self) -> None:
|
||||||
user = self.example_user("hamlet")
|
user = self.example_user("hamlet")
|
||||||
bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
|
bot = get_system_bot(settings.WELCOME_BOT, user.realm_id)
|
||||||
|
|
|
@ -215,13 +215,6 @@ def home_real(request: HttpRequest) -> HttpResponse:
|
||||||
|
|
||||||
narrow, narrow_stream, narrow_topic_name = detect_narrowed_window(request, user_profile)
|
narrow, narrow_stream, narrow_topic_name = detect_narrowed_window(request, user_profile)
|
||||||
|
|
||||||
if user_profile is not None:
|
|
||||||
needs_tutorial = user_profile.tutorial_status == UserProfile.TUTORIAL_WAITING
|
|
||||||
|
|
||||||
else:
|
|
||||||
# The current tutorial doesn't super make sense for logged-out users.
|
|
||||||
needs_tutorial = False
|
|
||||||
|
|
||||||
queue_id, page_params = build_page_params_for_home_page_load(
|
queue_id, page_params = build_page_params_for_home_page_load(
|
||||||
request=request,
|
request=request,
|
||||||
user_profile=user_profile,
|
user_profile=user_profile,
|
||||||
|
@ -230,7 +223,6 @@ def home_real(request: HttpRequest) -> HttpResponse:
|
||||||
narrow=narrow,
|
narrow=narrow,
|
||||||
narrow_stream=narrow_stream,
|
narrow_stream=narrow_stream,
|
||||||
narrow_topic_name=narrow_topic_name,
|
narrow_topic_name=narrow_topic_name,
|
||||||
needs_tutorial=needs_tutorial,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
log_data = RequestNotes.get_notes(request).log_data
|
log_data = RequestNotes.get_notes(request).log_data
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
|
||||||
|
|
||||||
from zerver.decorator import human_users_only
|
|
||||||
from zerver.lib.response import json_success
|
|
||||||
from zerver.lib.typed_endpoint import typed_endpoint
|
|
||||||
from zerver.models import UserProfile
|
|
||||||
|
|
||||||
|
|
||||||
@human_users_only
|
|
||||||
@typed_endpoint
|
|
||||||
def set_tutorial_status(
|
|
||||||
request: HttpRequest, user_profile: UserProfile, *, status: Literal["started", "finished"]
|
|
||||||
) -> HttpResponse:
|
|
||||||
if status == "started":
|
|
||||||
user_profile.tutorial_status = UserProfile.TUTORIAL_STARTED
|
|
||||||
elif status == "finished":
|
|
||||||
user_profile.tutorial_status = UserProfile.TUTORIAL_FINISHED
|
|
||||||
user_profile.save(update_fields=["tutorial_status"])
|
|
||||||
|
|
||||||
return json_success(request)
|
|
|
@ -176,7 +176,6 @@ from zerver.views.streams import (
|
||||||
)
|
)
|
||||||
from zerver.views.submessage import process_submessage
|
from zerver.views.submessage import process_submessage
|
||||||
from zerver.views.thumbnail import backend_serve_thumbnail
|
from zerver.views.thumbnail import backend_serve_thumbnail
|
||||||
from zerver.views.tutorial import set_tutorial_status
|
|
||||||
from zerver.views.typing import send_notification_backend
|
from zerver.views.typing import send_notification_backend
|
||||||
from zerver.views.unsubscribe import email_unsubscribe
|
from zerver.views.unsubscribe import email_unsubscribe
|
||||||
from zerver.views.upload import (
|
from zerver.views.upload import (
|
||||||
|
@ -426,16 +425,6 @@ v1_api_and_json_patterns = [
|
||||||
{"intentionally_undocumented"},
|
{"intentionally_undocumented"},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
# users/me/tutorial_status -> zerver.views.tutorial
|
|
||||||
rest_path(
|
|
||||||
"users/me/tutorial_status",
|
|
||||||
POST=(
|
|
||||||
set_tutorial_status,
|
|
||||||
# This is a relic of an old Zulip tutorial model and
|
|
||||||
# should be deleted.
|
|
||||||
{"intentionally_undocumented"},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
# settings -> zerver.views.user_settings
|
# settings -> zerver.views.user_settings
|
||||||
rest_path("settings", PATCH=json_change_settings),
|
rest_path("settings", PATCH=json_change_settings),
|
||||||
# These next two are legacy aliases for /settings, from before
|
# These next two are legacy aliases for /settings, from before
|
||||||
|
|
Loading…
Reference in New Issue