demo-orgs: Schedule onboarding/welcome emails when owner adds email.

When a demo organization creator confirms their email change for
their account, we now schedule the series of onboarding emails
via enqueue_welcome_emails.

If a demo organization is deactivated/deleted before the emails
are sent, any scheduled emails are deleted as part of that process.

We use the current datetime for getting the onboarding schedule
for the onboarding emails in this case, instead of the datetime the
user joined/created the organization.
This commit is contained in:
Lauryn Menard
2025-05-20 17:37:27 +02:00
committed by Tim Abbott
parent bf325ff6b2
commit 28b1a071fe
3 changed files with 54 additions and 18 deletions

View File

@@ -686,7 +686,9 @@ def handle_missedmessage_emails(
) )
def get_onboarding_email_schedule(user: UserProfile) -> dict[str, timedelta]: def get_onboarding_email_schedule(
user: UserProfile, demo_organization_creator: bool = False
) -> dict[str, timedelta]:
onboarding_emails = { onboarding_emails = {
# The delay should be 1 hour before the below specified number of days # The delay should be 1 hour before the below specified number of days
# as our goal is to maximize the chance that this email is near the top # as our goal is to maximize the chance that this email is near the top
@@ -700,7 +702,16 @@ def get_onboarding_email_schedule(user: UserProfile) -> dict[str, timedelta]:
user_tz = user.timezone user_tz = user.timezone
if user_tz == "": if user_tz == "":
user_tz = "UTC" user_tz = "UTC"
signup_day = user.date_joined.astimezone(
# Because demo organizations are created without setting an email for the
# owner's account, we schedule these emails when/if they add an email to
# their account.
if demo_organization_creator:
start_date = timezone_now()
else:
start_date = user.date_joined
day_of_week_user_registered = start_date.astimezone(
zoneinfo.ZoneInfo(canonicalize_timezone(user_tz)) zoneinfo.ZoneInfo(canonicalize_timezone(user_tz))
).isoweekday() ).isoweekday()
@@ -708,27 +719,27 @@ def get_onboarding_email_schedule(user: UserProfile) -> dict[str, timedelta]:
# -Do not send emails on Saturday or Sunday # -Do not send emails on Saturday or Sunday
# -Have at least one weekday between each (potential) email # -Have at least one weekday between each (potential) email
# User signed up on Monday # User registered on Monday
if signup_day == 1: if day_of_week_user_registered == 1:
# Send onboarding_team_to_zulip on Tuesday # Send onboarding_team_to_zulip on Tuesday
onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1) onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1)
# User signed up on Tuesday # User registered on Tuesday
if signup_day == 2: if day_of_week_user_registered == 2:
# Send onboarding_zulip_guide on Monday # Send onboarding_zulip_guide on Monday
onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1) onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1)
# Send onboarding_team_to_zulip on Wednesday # Send onboarding_team_to_zulip on Wednesday
onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1) onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1)
# User signed up on Wednesday # User registered on Wednesday
if signup_day == 3: if day_of_week_user_registered == 3:
# Send onboarding_zulip_guide on Tuesday # Send onboarding_zulip_guide on Tuesday
onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1) onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1)
# Send onboarding_team_to_zulip on Thursday # Send onboarding_team_to_zulip on Thursday
onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1) onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1)
# User signed up on Thursday # User registered on Thursday
if signup_day == 4: if day_of_week_user_registered == 4:
# Send onboarding_zulip_topics on Monday # Send onboarding_zulip_topics on Monday
onboarding_emails["onboarding_zulip_topics"] = timedelta(days=4, hours=-1) onboarding_emails["onboarding_zulip_topics"] = timedelta(days=4, hours=-1)
# Send onboarding_zulip_guide on Wednesday # Send onboarding_zulip_guide on Wednesday
@@ -736,8 +747,8 @@ def get_onboarding_email_schedule(user: UserProfile) -> dict[str, timedelta]:
# Send onboarding_team_to_zulip on Friday # Send onboarding_team_to_zulip on Friday
onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1) onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1)
# User signed up on Friday # User registered on Friday
if signup_day == 5: if day_of_week_user_registered == 5:
# Send onboarding_zulip_topics on Tuesday # Send onboarding_zulip_topics on Tuesday
onboarding_emails["onboarding_zulip_topics"] = timedelta(days=4, hours=-1) onboarding_emails["onboarding_zulip_topics"] = timedelta(days=4, hours=-1)
# Send onboarding_zulip_guide on Thursday # Send onboarding_zulip_guide on Thursday
@@ -745,10 +756,10 @@ def get_onboarding_email_schedule(user: UserProfile) -> dict[str, timedelta]:
# Send onboarding_team_to_zulip on Monday # Send onboarding_team_to_zulip on Monday
onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=10, hours=-1) onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=10, hours=-1)
# User signed up on Saturday; no adjustments needed # User registered on Saturday; no adjustments needed
# User signed up on Sunday # User registered on Sunday
if signup_day == 7: if day_of_week_user_registered == 7:
# Send onboarding_team_to_zulip on Monday # Send onboarding_team_to_zulip on Monday
onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1) onboarding_emails["onboarding_team_to_zulip"] = timedelta(days=8, hours=-1)
@@ -838,7 +849,9 @@ def send_account_registered_email(user: UserProfile, realm_creation: bool = Fals
) )
def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> None: def enqueue_welcome_emails(
user: UserProfile, realm_creation: bool = False, demo_organization_creator: bool = False
) -> None:
# Imported here to avoid import cycles. # Imported here to avoid import cycles.
from zerver.context_processors import common_context from zerver.context_processors import common_context
@@ -860,7 +873,7 @@ def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> N
# Any emails scheduled below should be added to the logic in get_onboarding_email_schedule # Any emails scheduled below should be added to the logic in get_onboarding_email_schedule
# to determine how long to delay sending the email based on when the user signed up. # to determine how long to delay sending the email based on when the user signed up.
onboarding_email_schedule = get_onboarding_email_schedule(user) onboarding_email_schedule = get_onboarding_email_schedule(user, demo_organization_creator)
if other_account_count == 0: if other_account_count == 0:
onboarding_zulip_topics_context = common_context(user) onboarding_zulip_topics_context = common_context(user)
@@ -915,7 +928,7 @@ def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> N
) )
# We only send the onboarding_team_to_zulip email to user who created the organization. # We only send the onboarding_team_to_zulip email to user who created the organization.
if realm_creation: if realm_creation or demo_organization_creator:
onboarding_team_to_zulip_context = common_context(user) onboarding_team_to_zulip_context = common_context(user)
onboarding_team_to_zulip_context.update( onboarding_team_to_zulip_context.update(
unsubscribe_link=unsubscribe_link, unsubscribe_link=unsubscribe_link,

View File

@@ -1,6 +1,7 @@
from datetime import timedelta from datetime import timedelta
from email.headerregistry import Address from email.headerregistry import Address
import orjson
import time_machine import time_machine
from django.conf import settings from django.conf import settings
from django.core import mail from django.core import mail
@@ -20,6 +21,7 @@ from zerver.actions.users import do_deactivate_user
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.models import EmailChangeStatus, UserProfile from zerver.models import EmailChangeStatus, UserProfile
from zerver.models.realms import get_realm from zerver.models.realms import get_realm
from zerver.models.scheduled_jobs import ScheduledEmail
from zerver.models.users import get_user, get_user_by_delivery_email, get_user_profile_by_id from zerver.models.users import get_user, get_user_by_delivery_email, get_user_profile_by_id
@@ -403,3 +405,20 @@ class EmailChangeTestCase(ZulipTestCase):
user_profile = get_user_profile_by_id(desdemona.id) user_profile = get_user_profile_by_id(desdemona.id)
self.assertEqual(user_profile.delivery_email, "desdemona-new@zulip.com") self.assertEqual(user_profile.delivery_email, "desdemona-new@zulip.com")
scheduled_emails = ScheduledEmail.objects.filter(users=user_profile).order_by(
"scheduled_timestamp"
)
self.assert_length(scheduled_emails, 3)
self.assertEqual(
orjson.loads(scheduled_emails[0].data)["template_prefix"],
"zerver/emails/onboarding_zulip_topics",
)
self.assertEqual(
orjson.loads(scheduled_emails[1].data)["template_prefix"],
"zerver/emails/onboarding_zulip_guide",
)
self.assertEqual(
orjson.loads(scheduled_emails[2].data)["template_prefix"],
"zerver/emails/onboarding_team_to_zulip",
)

View File

@@ -34,6 +34,7 @@ from zerver.actions.user_settings import (
from zerver.actions.users import generate_password_reset_url from zerver.actions.users import generate_password_reset_url
from zerver.decorator import human_users_only from zerver.decorator import human_users_only
from zerver.lib.avatar import avatar_url from zerver.lib.avatar import avatar_url
from zerver.lib.email_notifications import enqueue_welcome_emails
from zerver.lib.email_validation import ( from zerver.lib.email_validation import (
get_realm_email_validator, get_realm_email_validator,
validate_email_is_valid, validate_email_is_valid,
@@ -135,6 +136,9 @@ def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpRes
user_profile.realm.demo_organization_scheduled_deletion_date is not None user_profile.realm.demo_organization_scheduled_deletion_date is not None
and user_profile.is_realm_owner and user_profile.is_realm_owner
) )
# Schedule onboarding emails for demo organization owner now that we have an
# email address for their account.
enqueue_welcome_emails(user_profile, demo_organization_creator=True)
# Because demo organizations are created without setting an email and password # Because demo organizations are created without setting an email and password
# we want to redirect to setting a password after configuring and confirming # we want to redirect to setting a password after configuring and confirming
# an email for the owner's account. # an email for the owner's account.