mirror of https://github.com/zulip/zulip.git
onboarding_steps: Add 'OneTimeNotice' dataclass.
This commit adds a 'OneTimeNotice' dataclass to support one time banner and similar UI elements.
This commit is contained in:
parent
df379b5e86
commit
dde3d72100
|
@ -1,9 +1,9 @@
|
|||
from zerver.lib.hotspots import get_next_hotspots
|
||||
from zerver.lib.hotspots import get_next_onboarding_steps
|
||||
from zerver.models import OnboardingStep, UserProfile
|
||||
from zerver.tornado.django_api import send_event
|
||||
|
||||
|
||||
def do_mark_onboarding_step_as_read(user: UserProfile, onboarding_step: str) -> None:
|
||||
OnboardingStep.objects.get_or_create(user=user, onboarding_step=onboarding_step)
|
||||
event = dict(type="hotspots", hotspots=get_next_hotspots(user))
|
||||
event = dict(type="hotspots", hotspots=get_next_onboarding_steps(user))
|
||||
send_event(user.realm, event, [user.id])
|
||||
|
|
|
@ -18,7 +18,7 @@ from zerver.lib.compatibility import is_outdated_server
|
|||
from zerver.lib.default_streams import get_default_streams_for_realm_as_dicts
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.external_accounts import get_default_external_accounts
|
||||
from zerver.lib.hotspots import get_next_hotspots
|
||||
from zerver.lib.hotspots import get_next_onboarding_steps
|
||||
from zerver.lib.integrations import (
|
||||
EMBEDDED_BOTS,
|
||||
WEBHOOK_INTEGRATIONS,
|
||||
|
@ -188,7 +188,7 @@ def fetch_initial_state_data(
|
|||
# Even if we offered special hotspots for guests without an
|
||||
# account, we'd maybe need to store their state using cookies
|
||||
# or local storage, rather than in the database.
|
||||
state["hotspots"] = [] if user_profile is None else get_next_hotspots(user_profile)
|
||||
state["hotspots"] = [] if user_profile is None else get_next_onboarding_steps(user_profile)
|
||||
|
||||
if want("message"):
|
||||
# Since the introduction of `anchor="latest"` in the API,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# See https://zulip.readthedocs.io/en/latest/subsystems/hotspots.html
|
||||
# for documentation on this subsystem.
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
@ -67,14 +67,28 @@ INTRO_HOTSPOTS: List[Hotspot] = [
|
|||
|
||||
NON_INTRO_HOTSPOTS: List[Hotspot] = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class OneTimeNotice:
|
||||
name: str
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {
|
||||
"type": "one_time_notice",
|
||||
"name": self.name,
|
||||
}
|
||||
|
||||
|
||||
ONE_TIME_NOTICES: List[OneTimeNotice] = []
|
||||
|
||||
# We would most likely implement new hotspots in the future that aren't
|
||||
# a part of the initial tutorial. To that end, classifying them into
|
||||
# categories which are aggregated in ALL_HOTSPOTS, seems like a good start.
|
||||
ALL_HOTSPOTS = [*INTRO_HOTSPOTS, *NON_INTRO_HOTSPOTS]
|
||||
ALL_ONBOARDING_STEPS = ALL_HOTSPOTS
|
||||
ALL_ONBOARDING_STEPS: List[Union[Hotspot, OneTimeNotice]] = [*ALL_HOTSPOTS, *ONE_TIME_NOTICES]
|
||||
|
||||
|
||||
def get_next_hotspots(user: UserProfile) -> List[Dict[str, Union[str, float, bool]]]:
|
||||
def get_next_onboarding_steps(user: UserProfile) -> List[Dict[str, Any]]:
|
||||
# For manual testing, it can be convenient to set
|
||||
# ALWAYS_SEND_ALL_HOTSPOTS=True in `zproject/dev_settings.py` to
|
||||
# make it easy to click on all of the hotspots.
|
||||
|
@ -88,25 +102,30 @@ def get_next_hotspots(user: UserProfile) -> List[Dict[str, Union[str, float, boo
|
|||
if not settings.TUTORIAL_ENABLED:
|
||||
return []
|
||||
|
||||
seen_hotspots = frozenset(
|
||||
seen_onboarding_steps = frozenset(
|
||||
OnboardingStep.objects.filter(user=user).values_list("onboarding_step", flat=True)
|
||||
)
|
||||
|
||||
hotspots = [hotspot.to_dict() for hotspot in NON_INTRO_HOTSPOTS]
|
||||
onboarding_steps: List[Dict[str, Any]] = [hotspot.to_dict() for hotspot in NON_INTRO_HOTSPOTS]
|
||||
|
||||
for one_time_notice in ONE_TIME_NOTICES:
|
||||
if one_time_notice.name in seen_onboarding_steps:
|
||||
continue
|
||||
onboarding_steps.append(one_time_notice.to_dict())
|
||||
|
||||
if user.tutorial_status == UserProfile.TUTORIAL_FINISHED:
|
||||
return hotspots
|
||||
return onboarding_steps
|
||||
|
||||
for hotspot in INTRO_HOTSPOTS:
|
||||
if hotspot.name in seen_hotspots:
|
||||
if hotspot.name in seen_onboarding_steps:
|
||||
continue
|
||||
|
||||
hotspots.append(hotspot.to_dict(delay=0.5))
|
||||
return hotspots
|
||||
onboarding_steps.append(hotspot.to_dict(delay=0.5))
|
||||
return onboarding_steps
|
||||
|
||||
user.tutorial_status = UserProfile.TUTORIAL_FINISHED
|
||||
user.save(update_fields=["tutorial_status"])
|
||||
return hotspots
|
||||
return onboarding_steps
|
||||
|
||||
|
||||
def copy_hotspots(source_profile: UserProfile, target_profile: UserProfile) -> None:
|
||||
|
|
|
@ -2,14 +2,20 @@ from typing_extensions import override
|
|||
|
||||
from zerver.actions.create_user import do_create_user
|
||||
from zerver.actions.hotspots import do_mark_onboarding_step_as_read
|
||||
from zerver.lib.hotspots import ALL_HOTSPOTS, INTRO_HOTSPOTS, NON_INTRO_HOTSPOTS, get_next_hotspots
|
||||
from zerver.lib.hotspots import (
|
||||
ALL_HOTSPOTS,
|
||||
INTRO_HOTSPOTS,
|
||||
NON_INTRO_HOTSPOTS,
|
||||
ONE_TIME_NOTICES,
|
||||
get_next_onboarding_steps,
|
||||
)
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.models import OnboardingStep, UserProfile, get_realm
|
||||
|
||||
|
||||
# Splitting this out, since I imagine this will eventually have most of the
|
||||
# complicated hotspots logic.
|
||||
class TestGetNextHotspots(ZulipTestCase):
|
||||
# complicated onboarding steps logic.
|
||||
class TestGetNextOnboardingSteps(ZulipTestCase):
|
||||
@override
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
@ -18,37 +24,47 @@ class TestGetNextHotspots(ZulipTestCase):
|
|||
)
|
||||
|
||||
def test_first_hotspot(self) -> None:
|
||||
hotspots = get_next_hotspots(self.user)
|
||||
for hotspot in NON_INTRO_HOTSPOTS: # nocoverage
|
||||
do_mark_onboarding_step_as_read(self.user, hotspot.name)
|
||||
|
||||
for one_time_notice in ONE_TIME_NOTICES: # nocoverage
|
||||
do_mark_onboarding_step_as_read(self.user, one_time_notice.name)
|
||||
|
||||
hotspots = get_next_onboarding_steps(self.user)
|
||||
self.assert_length(hotspots, 1)
|
||||
self.assertEqual(hotspots[0]["name"], "intro_streams")
|
||||
|
||||
def test_some_done_some_not(self) -> None:
|
||||
do_mark_onboarding_step_as_read(self.user, "intro_streams")
|
||||
do_mark_onboarding_step_as_read(self.user, "intro_compose")
|
||||
hotspots = get_next_hotspots(self.user)
|
||||
self.assert_length(hotspots, 1)
|
||||
self.assertEqual(hotspots[0]["name"], "intro_topics")
|
||||
onboarding_steps = get_next_onboarding_steps(self.user)
|
||||
self.assert_length(onboarding_steps, 1)
|
||||
self.assertEqual(onboarding_steps[0]["name"], "intro_topics")
|
||||
|
||||
def test_all_hotspots_done(self) -> None:
|
||||
def test_all_onboarding_steps_done(self) -> None:
|
||||
with self.settings(TUTORIAL_ENABLED=True):
|
||||
self.assertNotEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
||||
for hotspot in NON_INTRO_HOTSPOTS: # nocoverage
|
||||
do_mark_onboarding_step_as_read(self.user, hotspot.name)
|
||||
|
||||
self.assertNotEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
||||
for one_time_notice in ONE_TIME_NOTICES: # nocoverage
|
||||
do_mark_onboarding_step_as_read(self.user, one_time_notice.name)
|
||||
|
||||
self.assertNotEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
||||
for hotspot in INTRO_HOTSPOTS:
|
||||
do_mark_onboarding_step_as_read(self.user, hotspot.name)
|
||||
|
||||
self.assertEqual(self.user.tutorial_status, UserProfile.TUTORIAL_FINISHED)
|
||||
self.assertEqual(get_next_hotspots(self.user), [])
|
||||
self.assertEqual(get_next_onboarding_steps(self.user), [])
|
||||
|
||||
def test_send_all(self) -> None:
|
||||
def test_send_all_hotspots(self) -> None:
|
||||
with self.settings(DEVELOPMENT=True, ALWAYS_SEND_ALL_HOTSPOTS=True):
|
||||
self.assert_length(ALL_HOTSPOTS, len(get_next_hotspots(self.user)))
|
||||
self.assert_length(ALL_HOTSPOTS, len(get_next_onboarding_steps(self.user)))
|
||||
|
||||
def test_tutorial_disabled(self) -> None:
|
||||
with self.settings(TUTORIAL_ENABLED=False):
|
||||
self.assertEqual(get_next_hotspots(self.user), [])
|
||||
self.assertEqual(get_next_onboarding_steps(self.user), [])
|
||||
|
||||
|
||||
class TestOnboardingSteps(ZulipTestCase):
|
||||
|
|
Loading…
Reference in New Issue