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:
Prakhar Pratyush 2024-07-25 12:37:44 +05:30 committed by Tim Abbott
parent ee806c49b9
commit 52a9846cdf
19 changed files with 26 additions and 124 deletions

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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");
}
}

View File

@ -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();

View File

@ -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,
) )

View File

@ -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,

View File

@ -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,

View File

@ -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"])

View File

@ -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",
),
]

View File

@ -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()

View File

@ -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)

View File

@ -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",

View File

@ -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
) )

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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