diff --git a/templates/zerver/emails/onboarding_zulip_guide.html b/templates/zerver/emails/onboarding_zulip_guide.html new file mode 100644 index 0000000000..0ee25e52e8 --- /dev/null +++ b/templates/zerver/emails/onboarding_zulip_guide.html @@ -0,0 +1,39 @@ +{% extends "zerver/emails/email_base_default.html" %} + +{% block illustration %} + +{% endblock %} + +{% block content %} + +
{{ _("We hope you have been enjoying Zulip so far!") }}
+ +{{ _("As you are getting started, check out this guide to key Zulip features for organizations like yours.") }}
+ +{% if organization_type == "business" %} +{{ _("View Zulip guide for businesses") }} +{% elif organization_type == "opensource" %} +{{ _("View Zulip guide for open-source projects") }} +{% elif organization_type == "education" %} +{{ _("View Zulip guide for education") }} +{% elif organization_type == "research" %} +{{ _("View Zulip guide for research") }} +{% elif organization_type == "event" %} +{{ _("View Zulip guide for events and conferences") }} +{% elif organization_type == "nonprofit" %} +{{ _("View Zulip guide for non-profits") }} +{% elif organization_type == "community" %} +{{ _("View Zulip guide for communities") }} +{% endif %} + ++ {% trans support_email=macros.email_tag(support_email) %}Questions? Contact us any time at {{ support_email }}.{% endtrans %} +
+ +{% endblock %} + +{% block manage_preferences %} + +{% trans %}Unsubscribe from welcome emails for {{ realm_name }}{% endtrans %}
+ +{% endblock %} diff --git a/templates/zerver/emails/onboarding_zulip_guide.subject.txt b/templates/zerver/emails/onboarding_zulip_guide.subject.txt new file mode 100644 index 0000000000..2e88fb21c8 --- /dev/null +++ b/templates/zerver/emails/onboarding_zulip_guide.subject.txt @@ -0,0 +1,15 @@ +{% if organization_type == "business" %} +{{ _("Zulip guide for businesses") }} +{% elif organization_type == "opensource" %} +{{ _("Zulip guide for open-source projects") }} +{% elif organization_type == "education" %} +{{ _("Zulip guide for education") }} +{% elif organization_type == "research" %} +{{ _("Zulip guide for research") }} +{% elif organization_type == "event" %} +{{ _("Zulip guide for events and conferences") }} +{% elif organization_type == "nonprofit" %} +{{ _("Zulip guide for non-profits") }} +{% elif organization_type == "community" %} +{{ _("Zulip guide for communities") }} +{% endif %} diff --git a/templates/zerver/emails/onboarding_zulip_guide.txt b/templates/zerver/emails/onboarding_zulip_guide.txt new file mode 100644 index 0000000000..c3ca153eea --- /dev/null +++ b/templates/zerver/emails/onboarding_zulip_guide.txt @@ -0,0 +1,26 @@ +{{ _("We hope you have been enjoying Zulip so far!") }} + +{{ _("As you are getting started, check out this guide to key Zulip features for organizations like yours.") }} + +{% if organization_type == "business" %} +{{ _("View Zulip guide for businesses") }}: +{% elif organization_type == "opensource" %} +{{ _("View Zulip guide for open-source projects") }}: +{% elif organization_type == "education" %} +{{ _("View Zulip guide for education") }}: +{% elif organization_type == "research" %} +{{ _("View Zulip guide for research") }}: +{% elif organization_type == "event" %} +{{ _("View Zulip guide for events and conferences") }}: +{% elif organization_type == "nonprofit" %} +{{ _("View Zulip guide for non-profits") }}: +{% elif organization_type == "community" %} +{{ _("View Zulip guide for communities") }}: +{% endif %} +<{{ zulip_guide_link }}> + +{% trans %}Questions? Contact us any time at {{ support_email }}.{% endtrans %} + +---- +{% trans %}Unsubscribe from welcome emails for {{ realm_name }}{% endtrans %}: +{{ unsubscribe_link }} diff --git a/zerver/lib/email_notifications.py b/zerver/lib/email_notifications.py index b9aa6d6ad5..13d4079509 100644 --- a/zerver/lib/email_notifications.py +++ b/zerver/lib/email_notifications.py @@ -37,6 +37,7 @@ from zerver.lib.url_encoding import ( ) from zerver.models import ( Message, + Realm, Recipient, Stream, UserMessage, @@ -686,6 +687,7 @@ def get_onboarding_email_schedule(user: UserProfile) -> Dict[str, timedelta]: # of the user's inbox when the user sits down to deal with their inbox, # or comes in while they are dealing with their inbox. "followup_day2": timedelta(days=2, hours=-1), + "onboarding_zulip_guide": timedelta(days=4, hours=-1), } user_tz = user.timezone @@ -693,19 +695,56 @@ def get_onboarding_email_schedule(user: UserProfile) -> Dict[str, timedelta]: user_tz = "UTC" signup_day = user.date_joined.astimezone(zoneinfo.ZoneInfo(user_tz)).isoweekday() + # General rules for scheduling welcome emails flow: + # -Do not send emails on Saturday or Sunday + # -Have at least one weekday between each (potential) email + + # User signed up on Tuesday + if signup_day == 2: + # Send followup_day2 on Thursday + # Send onboarding_zulip_guide on Monday + onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1) + + # User signed up on Wednesday + if signup_day == 3: + # Send followup_day2 on Friday + # Send onboarding_zulip_guide on Tuesday + onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1) + # User signed up on Thursday if signup_day == 4: - # Send followup_day2 on Friday - onboarding_emails["followup_day2"] = timedelta(days=1, hours=-1) + # Send followup_day2 on Monday + onboarding_emails["followup_day2"] = timedelta(days=4, hours=-1) + # Send onboarding_zulip_guide on Wednesday + onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1) # User signed up on Friday if signup_day == 5: - # Send followup_day2 on Monday - onboarding_emails["followup_day2"] = timedelta(days=3, hours=-1) + # Send followup_day2 on Tuesday + onboarding_emails["followup_day2"] = timedelta(days=4, hours=-1) + # Send onboarding_zulip_guide on Thursday + onboarding_emails["onboarding_zulip_guide"] = timedelta(days=6, hours=-1) return onboarding_emails +def get_org_type_zulip_guide(realm: Realm) -> Tuple[Any, str]: + for realm_type, realm_type_details in Realm.ORG_TYPES.items(): + if realm_type_details["id"] == realm.org_type: + organization_type_in_template = realm_type + + # There are two education organization types that receive the same email + # content, so we simplify to one shared template context value here. + if organization_type_in_template == "education_nonprofit": + organization_type_in_template = "education" + + return (realm_type_details["onboarding_zulip_guide_url"], organization_type_in_template) + + # Log problem, and return values that will not send onboarding_zulip_guide email. + logging.error("Unknown organization type '%s'", realm.org_type) + return (None, "") + + def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> None: from zerver.context_processors import common_context @@ -776,6 +815,38 @@ def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> N delay=onboarding_email_schedule["followup_day2"], ) + # We only send the onboarding_zulip_guide email for a subset of Realm.ORG_TYPES + onboarding_zulip_guide_url, organization_type_reference = get_org_type_zulip_guide(user.realm) + + # Only send follow_zulip_guide to "/for/communities/" guide if user is realm admin. + # TODO: Remove this condition and related tests when guide is updated; + # see https://github.com/zulip/zulip/issues/24822. + if ( + onboarding_zulip_guide_url == Realm.ORG_TYPES["community"]["onboarding_zulip_guide_url"] + and not user.is_realm_admin + ): + onboarding_zulip_guide_url = None + + if onboarding_zulip_guide_url is not None: + onboarding_zulip_guide_context = common_context(user) + onboarding_zulip_guide_context.update( + # We use the same unsubscribe link in both followup_day2 + # and onboarding_zulip_guide as these links do not expire. + unsubscribe_link=unsubscribe_link, + organization_type=organization_type_reference, + zulip_guide_link=onboarding_zulip_guide_url, + ) + + send_future_email( + "zerver/emails/onboarding_zulip_guide", + user.realm, + to_user_ids=[user.id], + from_name=from_name, + from_address=from_address, + context=onboarding_zulip_guide_context, + delay=onboarding_email_schedule["onboarding_zulip_guide"], + ) + def convert_html_to_markdown(html: str) -> str: # html2text is GPL licensed, so run it as a subprocess. diff --git a/zerver/models.py b/zerver/models.py index 4a07fae14a..425d62b79e 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -515,78 +515,91 @@ class Realm(models.Model): # type: ignore[django-manager-missing] # django-stub "id": 0, "hidden": True, "display_order": 0, + "onboarding_zulip_guide_url": None, }, "business": { "name": "Business", "id": 10, "hidden": False, "display_order": 1, + "onboarding_zulip_guide_url": "https://zulip.com/for/business/", }, "opensource": { "name": "Open-source project", "id": 20, "hidden": False, "display_order": 2, + "onboarding_zulip_guide_url": "https://zulip.com/for/open-source/", }, "education_nonprofit": { "name": "Education (non-profit)", "id": 30, "hidden": False, "display_order": 3, + "onboarding_zulip_guide_url": "https://zulip.com/for/education/", }, "education": { "name": "Education (for-profit)", "id": 35, "hidden": False, "display_order": 4, + "onboarding_zulip_guide_url": "https://zulip.com/for/education/", }, "research": { "name": "Research", "id": 40, "hidden": False, "display_order": 5, + "onboarding_zulip_guide_url": "https://zulip.com/for/research/", }, "event": { "name": "Event or conference", "id": 50, "hidden": False, "display_order": 6, + "onboarding_zulip_guide_url": "https://zulip.com/for/events/", }, "nonprofit": { "name": "Non-profit (registered)", "id": 60, "hidden": False, "display_order": 7, + "onboarding_zulip_guide_url": "https://zulip.com/for/communities/", }, "government": { "name": "Government", "id": 70, "hidden": False, "display_order": 8, + "onboarding_zulip_guide_url": None, }, "political_group": { "name": "Political group", "id": 80, "hidden": False, "display_order": 9, + "onboarding_zulip_guide_url": None, }, "community": { "name": "Community", "id": 90, "hidden": False, "display_order": 10, + "onboarding_zulip_guide_url": "https://zulip.com/for/communities/", }, "personal": { "name": "Personal", "id": 100, "hidden": False, "display_order": 100, + "onboarding_zulip_guide_url": None, }, "other": { "name": "Other", "id": 1000, "hidden": False, "display_order": 1000, + "onboarding_zulip_guide_url": None, }, } @@ -4345,6 +4358,7 @@ class ScheduledMessage(models.Model): EMAIL_TYPES = { "followup_day1": ScheduledEmail.WELCOME, "followup_day2": ScheduledEmail.WELCOME, + "onboarding_zulip_guide": ScheduledEmail.WELCOME, "digest": ScheduledEmail.DIGEST, "invitation_reminder": ScheduledEmail.INVITATION_REMINDER, } diff --git a/zerver/tests/test_email_log.py b/zerver/tests/test_email_log.py index 2509209496..0e2bdce2eb 100644 --- a/zerver/tests/test_email_log.py +++ b/zerver/tests/test_email_log.py @@ -26,7 +26,7 @@ class EmailLogTest(ZulipTestCase): output_log = ( "INFO:root:Emails sent in development are available at http://testserver/emails" ) - self.assertEqual(m.output, [output_log for i in range(15)]) + self.assertEqual(m.output, [output_log for i in range(17)]) def test_forward_address_details(self) -> None: try: diff --git a/zerver/tests/test_email_notifications.py b/zerver/tests/test_email_notifications.py index 92d9c162f7..3824de89a8 100644 --- a/zerver/tests/test_email_notifications.py +++ b/zerver/tests/test_email_notifications.py @@ -32,7 +32,7 @@ from zerver.lib.email_notifications import ( ) from zerver.lib.send_email import FromAddress, deliver_scheduled_emails, send_custom_email from zerver.lib.test_classes import ZulipTestCase -from zerver.models import ScheduledEmail, UserMessage, UserProfile, get_realm, get_stream +from zerver.models import Realm, ScheduledEmail, UserMessage, UserProfile, get_realm, get_stream class TestCustomEmails(ZulipTestCase): @@ -224,7 +224,9 @@ class TestFollowupEmails(ZulipTestCase): def test_day1_email_context(self) -> None: hamlet = self.example_user("hamlet") enqueue_welcome_emails(hamlet) - scheduled_emails = ScheduledEmail.objects.filter(users=hamlet) + scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by( + "scheduled_timestamp" + ) email_data = orjson.loads(scheduled_emails[0].data) self.assertEqual(email_data["context"]["email"], self.example_email("hamlet")) self.assertEqual(email_data["context"]["is_realm_admin"], False) @@ -238,7 +240,7 @@ class TestFollowupEmails(ZulipTestCase): iago = self.example_user("iago") enqueue_welcome_emails(iago) - scheduled_emails = ScheduledEmail.objects.filter(users=iago) + scheduled_emails = ScheduledEmail.objects.filter(users=iago).order_by("scheduled_timestamp") email_data = orjson.loads(scheduled_emails[0].data) self.assertEqual(email_data["context"]["email"], self.example_email("iago")) self.assertEqual(email_data["context"]["is_realm_admin"], True) @@ -274,9 +276,11 @@ class TestFollowupEmails(ZulipTestCase): self.ldap_password("newuser_email_as_uid@zulip.com"), ) user = UserProfile.objects.get(delivery_email="newuser_email_as_uid@zulip.com") - scheduled_emails = ScheduledEmail.objects.filter(users=user) + scheduled_emails = ScheduledEmail.objects.filter(users=user).order_by( + "scheduled_timestamp" + ) - self.assert_length(scheduled_emails, 2) + self.assert_length(scheduled_emails, 3) email_data = orjson.loads(scheduled_emails[0].data) self.assertEqual(email_data["context"]["ldap"], True) self.assertEqual( @@ -300,9 +304,10 @@ class TestFollowupEmails(ZulipTestCase): self.login_with_return("newuser@zulip.com", self.ldap_password("newuser")) user = UserProfile.objects.get(delivery_email="newuser@zulip.com") - scheduled_emails = ScheduledEmail.objects.filter(users=user) - - self.assert_length(scheduled_emails, 2) + scheduled_emails = ScheduledEmail.objects.filter(users=user).order_by( + "scheduled_timestamp" + ) + self.assert_length(scheduled_emails, 3) email_data = orjson.loads(scheduled_emails[0].data) self.assertEqual(email_data["context"]["ldap"], True) self.assertEqual(email_data["context"]["ldap_username"], "newuser") @@ -323,49 +328,129 @@ class TestFollowupEmails(ZulipTestCase): ): self.login_with_return("newuser_with_email", self.ldap_password("newuser_with_email")) user = UserProfile.objects.get(delivery_email="newuser_email@zulip.com") - scheduled_emails = ScheduledEmail.objects.filter(users=user) - - self.assert_length(scheduled_emails, 2) + scheduled_emails = ScheduledEmail.objects.filter(users=user).order_by( + "scheduled_timestamp" + ) + self.assert_length(scheduled_emails, 3) email_data = orjson.loads(scheduled_emails[0].data) self.assertEqual(email_data["context"]["ldap"], True) self.assertEqual(email_data["context"]["ldap_username"], "newuser_with_email") def test_followup_emails_count(self) -> None: hamlet = self.example_user("hamlet") + iago = self.example_user("iago") cordelia = self.example_user("cordelia") + realm = get_realm("zulip") + # Hamlet has account only in Zulip realm so day1, day2 and zulip_guide emails should be sent + enqueue_welcome_emails(self.example_user("hamlet")) + scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by( + "scheduled_timestamp" + ) + self.assert_length(scheduled_emails, 3) + self.assertEqual( + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + ) + self.assertEqual( + orjson.loads(scheduled_emails[1].data)["template_prefix"], "zerver/emails/followup_day2" + ) + self.assertEqual( + orjson.loads(scheduled_emails[2].data)["template_prefix"], + "zerver/emails/onboarding_zulip_guide", + ) + + ScheduledEmail.objects.all().delete() + + # The onboarding_zulip_guide email should not be sent to non-admin users in organizations + # that are sent the `/for/communities/` guide; see note in enqueue_welcome_emails. + realm.org_type = Realm.ORG_TYPES["community"]["id"] + realm.save() + + # Hamlet is not an admin so the `/for/communities/` zulip_guide should not be sent enqueue_welcome_emails(self.example_user("hamlet")) - # Hamlet has account only in Zulip realm so both day1 and day2 emails should be sent scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by( "scheduled_timestamp" ) self.assert_length(scheduled_emails, 2) self.assertEqual( - orjson.loads(scheduled_emails[1].data)["template_prefix"], "zerver/emails/followup_day2" + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" ) self.assertEqual( - orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + orjson.loads(scheduled_emails[1].data)["template_prefix"], "zerver/emails/followup_day2" ) ScheduledEmail.objects.all().delete() - enqueue_welcome_emails(cordelia) - scheduled_emails = ScheduledEmail.objects.filter(users=cordelia) + # Iago is an admin so the `/for/communities/` zulip_guide should be sent + enqueue_welcome_emails(self.example_user("iago")) + scheduled_emails = ScheduledEmail.objects.filter(users=iago).order_by("scheduled_timestamp") + self.assert_length(scheduled_emails, 3) + self.assertEqual( + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + ) + self.assertEqual( + orjson.loads(scheduled_emails[1].data)["template_prefix"], "zerver/emails/followup_day2" + ) + self.assertEqual( + orjson.loads(scheduled_emails[2].data)["template_prefix"], + "zerver/emails/onboarding_zulip_guide", + ) + + ScheduledEmail.objects.all().delete() + + # The organization_type context for "education_nonprofit" orgs is simplified to be "education" + realm.org_type = Realm.ORG_TYPES["education_nonprofit"]["id"] + realm.save() + # Cordelia has account in more than 1 realm so day2 email should not be sent + enqueue_welcome_emails(self.example_user("cordelia")) + scheduled_emails = ScheduledEmail.objects.filter(users=cordelia).order_by( + "scheduled_timestamp" + ) + self.assert_length(scheduled_emails, 2) + self.assertEqual( + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + ) + self.assertEqual( + orjson.loads(scheduled_emails[1].data)["template_prefix"], + "zerver/emails/onboarding_zulip_guide", + ) + self.assertEqual( + orjson.loads(scheduled_emails[1].data)["context"]["organization_type"], + "education", + ) + + ScheduledEmail.objects.all().delete() + + # Only a subset of Realm.ORG_TYPES are sent the zulip_guide_followup email + realm.org_type = Realm.ORG_TYPES["other"]["id"] + realm.save() + + # In this case, Cordelia should only be sent the day1 email + enqueue_welcome_emails(self.example_user("cordelia")) + scheduled_emails = ScheduledEmail.objects.filter(users=cordelia) self.assert_length(scheduled_emails, 1) - email_data = orjson.loads(scheduled_emails[0].data) - self.assertEqual(email_data["template_prefix"], "zerver/emails/followup_day1") + self.assertEqual( + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + ) def test_followup_emails_for_regular_realms(self) -> None: cordelia = self.example_user("cordelia") enqueue_welcome_emails(self.example_user("cordelia"), realm_creation=True) - scheduled_email = ScheduledEmail.objects.filter(users=cordelia).last() - assert scheduled_email is not None + scheduled_emails = ScheduledEmail.objects.filter(users=cordelia).order_by( + "scheduled_timestamp" + ) + assert scheduled_emails is not None + self.assert_length(scheduled_emails, 2) self.assertEqual( - orjson.loads(scheduled_email.data)["template_prefix"], "zerver/emails/followup_day1" + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + ) + self.assertEqual( + orjson.loads(scheduled_emails[1].data)["template_prefix"], + "zerver/emails/onboarding_zulip_guide", ) - deliver_scheduled_emails(scheduled_email) + deliver_scheduled_emails(scheduled_emails[0]) from django.core.mail import outbox self.assert_length(outbox, 1) @@ -381,13 +466,20 @@ class TestFollowupEmails(ZulipTestCase): ) cordelia.realm.save() enqueue_welcome_emails(self.example_user("cordelia"), realm_creation=True) - scheduled_email = ScheduledEmail.objects.filter(users=cordelia).last() - assert scheduled_email is not None + scheduled_emails = ScheduledEmail.objects.filter(users=cordelia).order_by( + "scheduled_timestamp" + ) + assert scheduled_emails is not None + self.assert_length(scheduled_emails, 2) self.assertEqual( - orjson.loads(scheduled_email.data)["template_prefix"], "zerver/emails/followup_day1" + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + ) + self.assertEqual( + orjson.loads(scheduled_emails[1].data)["template_prefix"], + "zerver/emails/onboarding_zulip_guide", ) - deliver_scheduled_emails(scheduled_email) + deliver_scheduled_emails(scheduled_emails[0]) from django.core.mail import outbox self.assert_length(outbox, 1) @@ -395,6 +487,27 @@ class TestFollowupEmails(ZulipTestCase): message = outbox[0] self.assertIn("you have created a new demo Zulip organization", message.body) + def test_onboarding_zulip_guide_with_invalid_org_type(self) -> None: + cordelia = self.example_user("cordelia") + realm = get_realm("zulip") + + invalid_org_type_id = 999 + realm.org_type = invalid_org_type_id + realm.save() + + with self.assertLogs(level="ERROR") as m: + enqueue_welcome_emails(self.example_user("cordelia")) + + scheduled_emails = ScheduledEmail.objects.filter(users=cordelia) + self.assert_length(scheduled_emails, 1) + self.assertEqual( + orjson.loads(scheduled_emails[0].data)["template_prefix"], "zerver/emails/followup_day1" + ) + self.assertEqual( + m.output, + [f"ERROR:root:Unknown organization type '{invalid_org_type_id}'"], + ) + class TestMissedMessages(ZulipTestCase): def test_read_message(self) -> None: @@ -1760,72 +1873,155 @@ class TestFollowupEmailDelay(ZulipTestCase): "Saturday": datetime(2018, 1, 6, 1, 0, 0, 0, tzinfo=timezone.utc), "Sunday": datetime(2018, 1, 7, 1, 0, 0, 0, tzinfo=timezone.utc), } + days_delayed = { + "2": timedelta(days=2, hours=-1), + "4": timedelta(days=4, hours=-1), + "6": timedelta(days=6, hours=-1), + } + # joined Monday user_profile.date_joined = dates_joined["Monday"] onboarding_email_schedule = get_onboarding_email_schedule(user_profile) + # followup_day2 email sent on Wednesday self.assertEqual( onboarding_email_schedule["followup_day2"], - timedelta(days=2, hours=-1), + days_delayed["2"], ) + self.assertEqual((dates_joined["Monday"] + days_delayed["2"]).isoweekday(), 3) + # onboarding_zulip_guide sent on Friday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["4"], + ) + self.assertEqual((dates_joined["Monday"] + days_delayed["4"]).isoweekday(), 5) + + # joined Tuesday user_profile.date_joined = dates_joined["Tuesday"] onboarding_email_schedule = get_onboarding_email_schedule(user_profile) + # followup_day2 email sent on Thursday self.assertEqual( onboarding_email_schedule["followup_day2"], - timedelta(days=2, hours=-1), + days_delayed["2"], ) + self.assertEqual((dates_joined["Tuesday"] + days_delayed["2"]).isoweekday(), 4) + # onboarding_zulip_guide sent on Monday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["6"], + ) + self.assertEqual((dates_joined["Tuesday"] + days_delayed["6"]).isoweekday(), 1) + + # joined Wednesday user_profile.date_joined = dates_joined["Wednesday"] onboarding_email_schedule = get_onboarding_email_schedule(user_profile) + # followup_day2 email sent on Friday self.assertEqual( onboarding_email_schedule["followup_day2"], - timedelta(days=2, hours=-1), + days_delayed["2"], ) + self.assertEqual((dates_joined["Wednesday"] + days_delayed["2"]).isoweekday(), 5) + # onboarding_zulip_guide sent on Tuesday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["6"], + ) + self.assertEqual((dates_joined["Wednesday"] + days_delayed["6"]).isoweekday(), 2) + + # joined Thursday user_profile.date_joined = dates_joined["Thursday"] onboarding_email_schedule = get_onboarding_email_schedule(user_profile) - # followup_day2 email sent on Friday + + # followup_day2 email sent on Monday self.assertEqual( onboarding_email_schedule["followup_day2"], - timedelta(days=1, hours=-1), + days_delayed["4"], ) + self.assertEqual((dates_joined["Thursday"] + days_delayed["4"]).isoweekday(), 1) + # onboarding_zulip_guide sent on Wednesday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["6"], + ) + self.assertEqual((dates_joined["Thursday"] + days_delayed["6"]).isoweekday(), 3) + + # joined Friday user_profile.date_joined = dates_joined["Friday"] onboarding_email_schedule = get_onboarding_email_schedule(user_profile) - # followup_day2 email sent on Monday - self.assertEqual( - onboarding_email_schedule["followup_day2"], - timedelta(days=3, hours=-1), - ) - user_profile.date_joined = dates_joined["Saturday"] - onboarding_email_schedule = get_onboarding_email_schedule(user_profile) - # followup_day2 email sent on Monday - self.assertEqual( - onboarding_email_schedule["followup_day2"], - timedelta(days=2, hours=-1), - ) - - user_profile.date_joined = dates_joined["Sunday"] - onboarding_email_schedule = get_onboarding_email_schedule(user_profile) # followup_day2 email sent on Tuesday self.assertEqual( onboarding_email_schedule["followup_day2"], - timedelta(days=2, hours=-1), + days_delayed["4"], ) + self.assertEqual((dates_joined["Friday"] + days_delayed["4"]).isoweekday(), 2) + + # onboarding_zulip_guide sent on Thursday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["6"], + ) + self.assertEqual((dates_joined["Friday"] + days_delayed["6"]).isoweekday(), 4) + + # joined Saturday + user_profile.date_joined = dates_joined["Saturday"] + onboarding_email_schedule = get_onboarding_email_schedule(user_profile) + + # followup_day2 email sent on Monday + self.assertEqual( + onboarding_email_schedule["followup_day2"], + days_delayed["2"], + ) + self.assertEqual((dates_joined["Saturday"] + days_delayed["2"]).isoweekday(), 1) + + # onboarding_zulip_guide sent on Wednesday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["4"], + ) + self.assertEqual((dates_joined["Saturday"] + days_delayed["4"]).isoweekday(), 3) + + # joined Sunday + user_profile.date_joined = dates_joined["Sunday"] + onboarding_email_schedule = get_onboarding_email_schedule(user_profile) + + # followup_day2 email sent on Tuesday + self.assertEqual( + onboarding_email_schedule["followup_day2"], + days_delayed["2"], + ) + self.assertEqual((dates_joined["Sunday"] + days_delayed["2"]).isoweekday(), 2) + + # onboarding_zulip_guide sent on Thursday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["4"], + ) + self.assertEqual((dates_joined["Sunday"] + days_delayed["4"]).isoweekday(), 4) # Time offset of America/Phoenix is -07:00 user_profile.timezone = "America/Phoenix" + # Test date_joined == Friday in UTC, but Thursday in the user's time zone user_profile.date_joined = datetime(2018, 1, 5, 1, 0, 0, 0, tzinfo=timezone.utc) onboarding_email_schedule = get_onboarding_email_schedule(user_profile) - # followup_day2 email sent on Friday + + # followup_day2 email sent on Monday self.assertEqual( onboarding_email_schedule["followup_day2"], - timedelta(days=1, hours=-1), + days_delayed["4"], + ) + + # onboarding_zulip_guide sent on Wednesday + self.assertEqual( + onboarding_email_schedule["onboarding_zulip_guide"], + days_delayed["6"], ) @@ -1841,7 +2037,9 @@ class TestCustomEmailSender(ZulipTestCase): ): hamlet = self.example_user("hamlet") enqueue_welcome_emails(hamlet) - scheduled_emails = ScheduledEmail.objects.filter(users=hamlet) + scheduled_emails = ScheduledEmail.objects.filter(users=hamlet).order_by( + "scheduled_timestamp" + ) email_data = orjson.loads(scheduled_emails[0].data) self.assertEqual(email_data["context"]["email"], self.example_email("hamlet")) self.assertEqual(email_data["from_name"], name) diff --git a/zerver/tests/test_example.py b/zerver/tests/test_example.py index 5f2a6be23e..ca409b892d 100644 --- a/zerver/tests/test_example.py +++ b/zerver/tests/test_example.py @@ -409,7 +409,7 @@ class TestDevelopmentEmailsLog(ZulipTestCase): "INFO:root:Emails sent in development are available at http://testserver/emails" ) # logger.output is a list of all the log messages captured. Verify it is as expected. - self.assertEqual(logger.output, [output_log] * 15) + self.assertEqual(logger.output, [output_log] * 17) # Now, lets actually go the URL the above call redirects to, i.e., /emails/ result = self.client_get(result["Location"]) diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 24cbb9f391..ac6f2275e1 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -939,7 +939,7 @@ class LoginTest(ZulipTestCase): ContentType.objects.clear_cache() # Ensure the number of queries we make is not O(streams) - with self.assert_database_query_count(94), cache_tries_captured() as cache_tries: + with self.assert_database_query_count(96), cache_tries_captured() as cache_tries: with self.captureOnCommitCallbacks(execute=True): self.register(self.nonreg_email("test"), "test") @@ -1134,9 +1134,9 @@ class EmailUnsubscribeTests(ZulipTestCase): click even when logged out to stop receiving them. """ user_profile = self.example_user("hamlet") - # Simulate a new user signing up, which enqueues 2 welcome e-mails. + # Simulate a new user signing up, which enqueues 3 welcome e-mails. enqueue_welcome_emails(user_profile) - self.assertEqual(2, ScheduledEmail.objects.filter(users=user_profile).count()) + self.assertEqual(3, ScheduledEmail.objects.filter(users=user_profile).count()) # Simulate unsubscribing from the welcome e-mails. unsubscribe_link = one_click_unsubscribe_link(user_profile, "welcome") diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py index 6bee57bc1d..bc2323edba 100644 --- a/zerver/tests/test_users.py +++ b/zerver/tests/test_users.py @@ -786,7 +786,7 @@ class QueryCountTest(ZulipTestCase): prereg_user = PreregistrationUser.objects.get(email="fred@zulip.com") - with self.assert_database_query_count(88): + with self.assert_database_query_count(90): with cache_tries_captured() as cache_tries: with self.capture_send_event_calls(expected_num_events=11) as events: fred = do_create_user(