From f391bfeec67322e420f0706db0e6dfe48d1a0fb6 Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Wed, 15 Mar 2023 20:18:09 +0100 Subject: [PATCH] emails: Add new onboarding email with guide for organization type. Adds a new welcome email, `onboarding_zulip_guide`, to be sent four days after a new user registers with a Zulip organization if the organization has specified a particular organization type that has a guide in the corporate `/for/.../` pages. If there is no guide, then no email is scheduled or sent. The current `for/communities/` page is not very useful for users who are not organization administrators, so these onboarding guide emails are further restricted for those organization types to only go to new users who are invited/registered as admins for the organzation. Adds two database queries for new user registrations: one to get the organization's type and one to create the scheduled email. Adds two email logs because the email is sent both to a new user who registers with an existing organization and to the organization owner when they register a new organization. Co-authored by: Alya Abbott --- .../zerver/emails/onboarding_zulip_guide.html | 39 +++ .../emails/onboarding_zulip_guide.subject.txt | 15 + .../zerver/emails/onboarding_zulip_guide.txt | 26 ++ zerver/lib/email_notifications.py | 79 ++++- zerver/models.py | 14 + zerver/tests/test_email_log.py | 2 +- zerver/tests/test_email_notifications.py | 298 +++++++++++++++--- zerver/tests/test_example.py | 2 +- zerver/tests/test_signup.py | 6 +- zerver/tests/test_users.py | 2 +- 10 files changed, 423 insertions(+), 60 deletions(-) create mode 100644 templates/zerver/emails/onboarding_zulip_guide.html create mode 100644 templates/zerver/emails/onboarding_zulip_guide.subject.txt create mode 100644 templates/zerver/emails/onboarding_zulip_guide.txt 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(