From 673a01ea0c9e02e6f3f8e132b18321f6677f034c Mon Sep 17 00:00:00 2001 From: Lauryn Menard Date: Mon, 25 Sep 2023 22:59:44 +0200 Subject: [PATCH] realm-deactivation: Send email to owners as part of deactivation. Creates a new "realm_deactivated" email that can be sent to realm owners as part of `do_deactivate_realm`, via a boolean flag, `email_owners`. This flag is set to `False` when `do_deactivate_realm` is used for realm exports or changing a realm's subdomain, so that the active organization owners are not emailed in those cases. This flag is optional for the `deactivate_realm` management command, but as there is no active user passed in that case, then the email is sent without referencing who deactivated the realm. It is passed as `True` for the support analytics view, but the email that is generated does not include information about the support admin user who completed the request for organization deactivation. When an active organization owner deactivates the organization, then the flag is `True` and an email is sent to them as well as any other active organization owners, with a slight variation in the email text for those two cases. Adds specific tests for when `email_owners` is passed as `True`. All existing tests for other functionality of `do_deactivate_user` pass the flag as `False`. Adds `localize` from django.util.formats as a jinja env filter so that the dates in these emails are internationlized for the owner's default language setting in the "realm_deactivated" email templates. Fixes #24685. --- corporate/tests/test_stripe.py | 10 +- corporate/tests/test_support_views.py | 1 + corporate/views/support.py | 5 +- .../zerver/emails/realm_deactivated.html | 21 ++++ .../emails/realm_deactivated.subject.txt | 1 + templates/zerver/emails/realm_deactivated.txt | 10 ++ tools/test-api | 4 +- zerver/actions/create_realm.py | 5 +- zerver/actions/realm_settings.py | 73 +++++++++++- .../management/commands/deactivate_realm.py | 13 ++- zerver/management/commands/export.py | 5 +- zerver/tests/test_audit_log.py | 4 +- zerver/tests/test_auth_backends.py | 25 +++- zerver/tests/test_decorators.py | 10 +- zerver/tests/test_email_change.py | 5 +- zerver/tests/test_email_mirror.py | 5 +- zerver/tests/test_events.py | 4 +- zerver/tests/test_push_notifications.py | 4 +- zerver/tests/test_realm.py | 109 +++++++++++++++--- zerver/tests/test_signup.py | 10 +- zerver/views/realm.py | 4 +- zproject/jinja2/__init__.py | 2 + 22 files changed, 290 insertions(+), 40 deletions(-) create mode 100644 templates/zerver/emails/realm_deactivated.html create mode 100644 templates/zerver/emails/realm_deactivated.subject.txt create mode 100644 templates/zerver/emails/realm_deactivated.txt diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index dfc408403c..eacbc6fbe4 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -4319,7 +4319,10 @@ class StripeTest(StripeTestCase): self.assertEqual(last_ledger_entry.licenses_at_next_renewal, 20) do_deactivate_realm( - get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" + get_realm("zulip"), + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) plan.refresh_from_db() @@ -4355,7 +4358,10 @@ class StripeTest(StripeTestCase): ) do_deactivate_realm( - get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" + get_realm("zulip"), + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) self.assertTrue(get_realm("zulip").deactivated) do_reactivate_realm(get_realm("zulip")) diff --git a/corporate/tests/test_support_views.py b/corporate/tests/test_support_views.py index 00821c9221..7b5af5e160 100644 --- a/corporate/tests/test_support_views.py +++ b/corporate/tests/test_support_views.py @@ -1426,6 +1426,7 @@ class TestSupportEndpoint(ZulipTestCase): lear_realm, acting_user=self.example_user("iago"), deactivation_reason="owner_request", + email_owners=True, ) self.assert_in_success_response(["lear deactivated"], result) diff --git a/corporate/views/support.py b/corporate/views/support.py index 6aa34b5c4a..dc1867feba 100644 --- a/corporate/views/support.py +++ b/corporate/views/support.py @@ -444,7 +444,10 @@ def support( # TODO: Add support for deactivation reason in the support UI that'll be passed # here. do_deactivate_realm( - realm, acting_user=acting_user, deactivation_reason="owner_request" + realm, + acting_user=acting_user, + deactivation_reason="owner_request", + email_owners=True, ) context["success_message"] = f"{realm.string_id} deactivated." elif scrub_realm: diff --git a/templates/zerver/emails/realm_deactivated.html b/templates/zerver/emails/realm_deactivated.html new file mode 100644 index 0000000000..59843ec8fe --- /dev/null +++ b/templates/zerver/emails/realm_deactivated.html @@ -0,0 +1,21 @@ +{% extends "zerver/emails/email_base_default.html" %} +{% set localized_date = event_date|localize %} + +{% block illustration %} + +{% endblock %} + +{% block content %} +

+ {% if acting_user and initiated_deactivation %} + {% trans %}You have deactivated your Zulip organization, {{ realm_name }}, on {{ localized_date }}.{% endtrans %} + {% elif acting_user %} + {% trans %}Your Zulip organization, {{ realm_name }}, was deactivated by {{ deactivating_owner }} on {{ localized_date }}.{% endtrans %} + {% else %} + {% trans %}Your Zulip organization, {{ realm_name }}, was deactivated on {{ localized_date }}.{% endtrans %} + {% endif %} +

+

+ {% trans %}If you have any questions or concerns, please reply to this email as soon as possible.{% endtrans %} +

+{% endblock %} diff --git a/templates/zerver/emails/realm_deactivated.subject.txt b/templates/zerver/emails/realm_deactivated.subject.txt new file mode 100644 index 0000000000..505c55147f --- /dev/null +++ b/templates/zerver/emails/realm_deactivated.subject.txt @@ -0,0 +1 @@ +{% trans %}Your Zulip organization {{ realm_name }} has been deactivated{% endtrans %} diff --git a/templates/zerver/emails/realm_deactivated.txt b/templates/zerver/emails/realm_deactivated.txt new file mode 100644 index 0000000000..d682241f54 --- /dev/null +++ b/templates/zerver/emails/realm_deactivated.txt @@ -0,0 +1,10 @@ +{% set localized_date = event_date|localize %} +{% if acting_user and initiated_deactivation %} + {% trans %}You have deactivated your Zulip organization, {{ realm_name }}, on {{ localized_date }}.{% endtrans %} +{% elif acting_user %} + {% trans %}Your Zulip organization, {{ realm_name }}, was deactivated by {{ deactivating_owner }} on {{ localized_date }}.{% endtrans %} +{% else %} + {% trans %}Your Zulip organization, {{ realm_name }}, was deactivated on {{ localized_date }}.{% endtrans %} +{% endif %} + +{% trans%}If you have any questions or concerns, please reply to this email as soon as possible.{% endtrans %} diff --git a/tools/test-api b/tools/test-api index 3dffb6a86a..4fab11f96f 100755 --- a/tools/test-api +++ b/tools/test-api @@ -116,7 +116,9 @@ with test_server_running( do_reactivate_user(guest_user, acting_user=None) # Test realm deactivated error - do_deactivate_realm(guest_user.realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + guest_user.realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) client = Client( email=email, diff --git a/zerver/actions/create_realm.py b/zerver/actions/create_realm.py index 6aee7a4077..b5db67dc11 100644 --- a/zerver/actions/create_realm.py +++ b/zerver/actions/create_realm.py @@ -98,7 +98,10 @@ def do_change_realm_subdomain( if add_deactivated_redirect: placeholder_realm = do_create_realm(old_subdomain, realm.name) do_deactivate_realm( - placeholder_realm, acting_user=None, deactivation_reason="subdomain_change" + placeholder_realm, + acting_user=None, + deactivation_reason="subdomain_change", + email_owners=False, ) do_add_deactivated_redirect(placeholder_realm, realm.url) diff --git a/zerver/actions/realm_settings.py b/zerver/actions/realm_settings.py index 0edd0c71b2..236ccb4fc8 100644 --- a/zerver/actions/realm_settings.py +++ b/zerver/actions/realm_settings.py @@ -2,8 +2,10 @@ import logging from email.headerregistry import Address from typing import Any, Dict, Literal, Optional, Tuple, Union +import zoneinfo from django.conf import settings from django.db import transaction +from django.utils.timezone import get_current_timezone_name as timezone_get_current_timezone_name from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ @@ -15,9 +17,10 @@ from zerver.actions.user_settings import do_delete_avatar_image from zerver.lib.exceptions import JsonableError from zerver.lib.message import parse_message_time_limit_setting, update_first_visible_message_id from zerver.lib.retention import move_messages_to_archive -from zerver.lib.send_email import FromAddress, send_email_to_admins +from zerver.lib.send_email import FromAddress, send_email, send_email_to_admins from zerver.lib.sessions import delete_realm_user_sessions from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime +from zerver.lib.timezone import canonicalize_timezone from zerver.lib.upload import delete_message_attachments from zerver.lib.user_counts import realm_user_count_by_role from zerver.lib.user_groups import ( @@ -501,6 +504,7 @@ def do_deactivate_realm( *, acting_user: Optional[UserProfile], deactivation_reason: RealmDeactivationReasonType, + email_owners: bool, ) -> None: """ Deactivate this realm. Do NOT deactivate the users -- we need to be able to @@ -557,6 +561,12 @@ def do_deactivate_realm( # declared in zerver/lib/safe_session_cached_db.py enforces this. delete_realm_user_sessions(realm) + # Flag to send deactivated realm email to organization owners; is false + # for realm exports and realm subdomain changes so that those actions + # do not email active organization owners. + if email_owners: + do_send_realm_deactivation_email(realm, acting_user) + def do_reactivate_realm(realm: Realm) -> None: if not realm.deactivated: @@ -800,3 +810,64 @@ def do_send_realm_reactivation_email(realm: Realm, *, acting_user: Optional[User language=language, context=context, ) + + +def do_send_realm_deactivation_email(realm: Realm, acting_user: Optional[UserProfile]) -> None: + shared_context: Dict[str, Any] = { + "realm_name": realm.name, + } + deactivation_time = timezone_now() + owners = set(realm.get_human_owner_users()) + anonymous_deactivation = False + + # The realm was deactivated via the deactivate_realm management command. + if acting_user is None: + anonymous_deactivation = True + + # This realm was deactivated from the support panel; we do not share the + # deactivating user's information in this case. + if acting_user is not None and acting_user not in owners: + anonymous_deactivation = True + + for owner in owners: + owner_tz = owner.timezone + if owner_tz == "": + owner_tz = timezone_get_current_timezone_name() + local_date = deactivation_time.astimezone( + zoneinfo.ZoneInfo(canonicalize_timezone(owner_tz)) + ).date() + + if anonymous_deactivation: + context = dict( + acting_user=False, + initiated_deactivation=False, + event_date=local_date, + **shared_context, + ) + else: + assert acting_user is not None + if owner == acting_user: + context = dict( + acting_user=True, + initiated_deactivation=True, + event_date=local_date, + **shared_context, + ) + else: + context = dict( + acting_user=True, + initiated_deactivation=False, + deactivating_owner=acting_user.full_name, + event_date=local_date, + **shared_context, + ) + + send_email( + "zerver/emails/realm_deactivated", + to_emails=[owner.delivery_email], + from_name=FromAddress.security_email_from_name(language=owner.default_language), + from_address=FromAddress.SUPPORT, + language=owner.default_language, + context=context, + realm=realm, + ) diff --git a/zerver/management/commands/deactivate_realm.py b/zerver/management/commands/deactivate_realm.py index 6f4ba0f739..90b5db743e 100644 --- a/zerver/management/commands/deactivate_realm.py +++ b/zerver/management/commands/deactivate_realm.py @@ -21,10 +21,15 @@ class Command(ZulipBaseCommand): help="Reason for deactivation", required=True, ) + parser.add_argument( + "--email_owners", + action="store_true", + help="Whether to email organization owners about realm deactivation", + ) self.add_realm_args(parser, required=True) @override - def handle(self, *args: Any, **options: str) -> None: + def handle(self, *args: Any, **options: Any) -> None: realm = self.get_realm(options) deactivation_reason = options["deactivation_reason"] @@ -38,8 +43,12 @@ class Command(ZulipBaseCommand): print("The realm", options["realm_id"], "is already deactivated.") return + send_realm_deactivation_email = options["email_owners"] print("Deactivating", options["realm_id"]) do_deactivate_realm( - realm, acting_user=None, deactivation_reason=cast(Any, deactivation_reason) + realm, + acting_user=None, + deactivation_reason=cast(Any, deactivation_reason), + email_owners=send_realm_deactivation_email, ) print("Done!") diff --git a/zerver/management/commands/export.py b/zerver/management/commands/export.py index 7bb3a8011b..327a937459 100644 --- a/zerver/management/commands/export.py +++ b/zerver/management/commands/export.py @@ -203,7 +203,10 @@ class Command(ZulipBaseCommand): if options["deactivate_realm"]: print(f"\033[94mDeactivating realm\033[0m: {realm.string_id}") do_deactivate_realm( - realm, acting_user=None, deactivation_reason="self_hosting_migration" + realm, + acting_user=None, + deactivation_reason="self_hosting_migration", + email_owners=False, ) def percent_callback(bytes_transferred: Any) -> None: diff --git a/zerver/tests/test_audit_log.py b/zerver/tests/test_audit_log.py index 848a49746a..11bedf7f17 100644 --- a/zerver/tests/test_audit_log.py +++ b/zerver/tests/test_audit_log.py @@ -420,7 +420,9 @@ class TestRealmAuditLog(ZulipTestCase): def test_realm_activation(self) -> None: realm = get_realm("zulip") user = self.example_user("desdemona") - do_deactivate_realm(realm, acting_user=user, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=user, deactivation_reason="owner_request", email_owners=False + ) log_entry = RealmAuditLog.objects.get( realm=realm, event_type=RealmAuditLog.REALM_DEACTIVATED, acting_user=user ) diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index ffd753c4b0..63d3304b69 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -215,7 +215,10 @@ class AuthBackendTest(ZulipTestCase): # Verify auth fails with a deactivated realm do_deactivate_realm( - user_profile.realm, acting_user=None, deactivation_reason="owner_request" + user_profile.realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) result = backend.authenticate(**good_kwargs) @@ -4992,7 +4995,10 @@ class FetchAPIKeyTest(ZulipTestCase): def test_deactivated_realm(self) -> None: do_deactivate_realm( - self.user_profile.realm, acting_user=None, deactivation_reason="owner_request" + self.user_profile.realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) result = self.client_post( "/api/v1/fetch_api_key", @@ -5056,7 +5062,10 @@ class DevFetchAPIKeyTest(ZulipTestCase): def test_deactivated_realm(self) -> None: do_deactivate_realm( - self.user_profile.realm, acting_user=None, deactivation_reason="owner_request" + self.user_profile.realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) result = self.client_post("/api/v1/dev_fetch_api_key", dict(username=self.email)) self.assert_json_error_contains(result, "This organization has been deactivated", 401) @@ -6350,7 +6359,10 @@ class TestLDAP(ZulipLDAPTestCase): backend = self.backend email = "nonexisting@zulip.com" do_deactivate_realm( - backend._realm, acting_user=None, deactivation_reason="owner_request" + backend._realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) with self.assertRaisesRegex(Exception, "Realm has been deactivated"): backend.get_or_build_user(email, _LDAPUser()) @@ -7473,7 +7485,10 @@ class JWTFetchAPIKeyTest(ZulipTestCase): def test_inactive_realm_failure(self) -> None: payload = {"email": self.email} do_deactivate_realm( - self.user_profile.realm, acting_user=None, deactivation_reason="owner_request" + self.user_profile.realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) with self.settings(JWT_AUTH_KEYS={"zulip": {"key": "key1", "algorithms": ["HS256"]}}): key = settings.JWT_AUTH_KEYS["zulip"]["key"] diff --git a/zerver/tests/test_decorators.py b/zerver/tests/test_decorators.py index ca7f7935d9..889d29e3d5 100644 --- a/zerver/tests/test_decorators.py +++ b/zerver/tests/test_decorators.py @@ -583,7 +583,10 @@ class DeactivatedRealmTest(ZulipTestCase): """ realm = get_realm("zulip") do_deactivate_realm( - get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" + get_realm("zulip"), + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) result = self.client_post( @@ -652,7 +655,10 @@ class DeactivatedRealmTest(ZulipTestCase): """ do_deactivate_realm( - get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" + get_realm("zulip"), + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) user_profile = self.example_user("hamlet") api_key = get_api_key(user_profile) diff --git a/zerver/tests/test_email_change.py b/zerver/tests/test_email_change.py index 5c91f9ad65..ba99fb3926 100644 --- a/zerver/tests/test_email_change.py +++ b/zerver/tests/test_email_change.py @@ -161,7 +161,10 @@ class EmailChangeTestCase(ZulipTestCase): activation_url = self.generate_email_change_link(new_email) do_deactivate_realm( - user_profile.realm, acting_user=None, deactivation_reason="owner_request" + user_profile.realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) response = self.client_get(activation_url) diff --git a/zerver/tests/test_email_mirror.py b/zerver/tests/test_email_mirror.py index 53486d1cb6..8297e01f7f 100644 --- a/zerver/tests/test_email_mirror.py +++ b/zerver/tests/test_email_mirror.py @@ -1283,7 +1283,10 @@ class TestMissedMessageEmailMessages(ZulipTestCase): mm_address = create_missed_message_address(user_profile, message) do_deactivate_realm( - user_profile.realm, acting_user=None, deactivation_reason="owner_request" + user_profile.realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) incoming_valid_message = EmailMessage() diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index d4928702a1..d0c35bd63a 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -3007,7 +3007,9 @@ class NormalActionsTest(BaseAction): # correct because were one to somehow compute page_params (as # this test does), but that's not actually possible. with self.verify_action(state_change_expected=False) as events: - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) check_realm_deactivated("events[0]", events[0]) def test_do_mark_onboarding_step_as_read(self) -> None: diff --git a/zerver/tests/test_push_notifications.py b/zerver/tests/test_push_notifications.py index f37548ed65..9377b4af00 100644 --- a/zerver/tests/test_push_notifications.py +++ b/zerver/tests/test_push_notifications.py @@ -1670,7 +1670,9 @@ class AnalyticsBouncerTest(BouncerTestCase): do_set_realm_authentication_methods(zephyr_realm, new_auth_method_dict, acting_user=user) # Deactivation is synced. - do_deactivate_realm(zephyr_realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + zephyr_realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) send_server_data_to_push_bouncer() check_counts(5, 5, 1, 1, 7) diff --git a/zerver/tests/test_realm.py b/zerver/tests/test_realm.py index 9525ff59ce..d5b36e0e29 100644 --- a/zerver/tests/test_realm.py +++ b/zerver/tests/test_realm.py @@ -9,6 +9,7 @@ from unittest import mock, skipUnless import orjson from django.conf import settings +from django.core import mail from django.test import override_settings from django.utils.timezone import now as timezone_now from typing_extensions import override @@ -314,7 +315,9 @@ class RealmTest(ZulipTestCase): hamlet_id = self.example_user("hamlet").id get_user_profile_by_id(hamlet_id) realm = get_realm("zulip") - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) user = get_user_profile_by_id(hamlet_id) self.assertTrue(user.realm.deactivated) @@ -361,7 +364,9 @@ class RealmTest(ZulipTestCase): delay=timedelta(hours=1), ) self.assertEqual(ScheduledEmail.objects.count(), 1) - do_deactivate_realm(user.realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + user.realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertEqual(ScheduledEmail.objects.count(), 0) def test_do_change_realm_description_clears_cached_descriptions(self) -> None: @@ -385,10 +390,14 @@ class RealmTest(ZulipTestCase): realm = get_realm("zulip") self.assertFalse(realm.deactivated) - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertTrue(realm.deactivated) - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertTrue(realm.deactivated) def test_do_set_deactivated_redirect_on_deactivated_realm(self) -> None: @@ -396,7 +405,9 @@ class RealmTest(ZulipTestCase): realm = get_realm("zulip") redirect_url = "new_server.zulip.com" - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertTrue(realm.deactivated) do_add_deactivated_redirect(realm, redirect_url) self.assertEqual(realm.deactivated_redirect, redirect_url) @@ -408,7 +419,9 @@ class RealmTest(ZulipTestCase): def test_do_reactivate_realm(self) -> None: realm = get_realm("zulip") - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertTrue(realm.deactivated) do_reactivate_realm(realm) @@ -438,7 +451,9 @@ class RealmTest(ZulipTestCase): def test_realm_reactivation_link(self) -> None: realm = get_realm("zulip") - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertTrue(realm.deactivated) obj = RealmReactivationStatus.objects.create(realm=realm) @@ -451,13 +466,17 @@ class RealmTest(ZulipTestCase): self.assertFalse(realm.deactivated) # Make sure the link can't be reused. - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) response = self.client_get(confirmation_url) self.assertEqual(response.status_code, 404) def test_realm_reactivation_confirmation_object(self) -> None: realm = get_realm("zulip") - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertTrue(realm.deactivated) obj = RealmReactivationStatus.objects.create(realm=realm) create_confirmation_link(obj, Confirmation.REALM_REACTIVATION) @@ -466,22 +485,80 @@ class RealmTest(ZulipTestCase): self.assertEqual(confirmation.content_object, obj) self.assertEqual(confirmation.realm, realm) + def test_do_send_realm_deactivation_email_no_acting_user(self) -> None: + realm = get_realm("zulip") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=True + ) + self.assertEqual(realm.deactivated, True) + self.assert_length(mail.outbox, 1) + self.assertIn( + "Your Zulip organization Zulip Dev has been deactivated", mail.outbox[0].subject + ) + self.assertIn("Your Zulip organization, Zulip Dev, was deactivated on", mail.outbox[0].body) + + def test_do_send_realm_deactivation_email_by_support(self) -> None: + realm = get_realm("lear") + king = self.lear_user("king") + king.role = UserProfile.ROLE_REALM_OWNER + king.save() + iago = self.example_user("iago") + do_deactivate_realm( + realm, acting_user=iago, deactivation_reason="owner_request", email_owners=True + ) + self.assertEqual(realm.deactivated, True) + self.assert_length(mail.outbox, 1) + self.assertIn( + "Your Zulip organization Lear & Co. has been deactivated", mail.outbox[0].subject + ) + self.assertIn( + "Your Zulip organization, Lear & Co., was deactivated on", + mail.outbox[0].body, + ) + + def test_do_send_realm_deactivation_email_by_owner(self) -> None: + realm = get_realm("zulip") + iago = self.example_user("iago") + iago.role = UserProfile.ROLE_REALM_OWNER + iago.save(update_fields=["role"]) + do_deactivate_realm( + realm, acting_user=iago, deactivation_reason="owner_request", email_owners=True + ) + self.assertEqual(realm.deactivated, True) + self.assert_length(mail.outbox, 2) + for email in mail.outbox: + if email.to[0] == "iago@zulip.com": + self.assertIn( + "Your Zulip organization Zulip Dev has been deactivated", email.subject + ) + self.assertIn( + "You have deactivated your Zulip organization, Zulip Dev, on", email.body + ) + else: + self.assertIn( + "Your Zulip organization Zulip Dev has been deactivated", email.subject + ) + self.assertIn( + "Your Zulip organization, Zulip Dev, was deactivated by Iago on", email.body + ) + def test_do_send_realm_reactivation_email(self) -> None: realm = get_realm("zulip") - do_deactivate_realm(realm, acting_user=None, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=None, deactivation_reason="owner_request", email_owners=False + ) self.assertEqual(realm.deactivated, True) iago = self.example_user("iago") do_send_realm_reactivation_email(realm, acting_user=iago) - from django.core.mail import outbox - self.assert_length(outbox, 1) - self.assertEqual(self.email_envelope_from(outbox[0]), settings.NOREPLY_EMAIL_ADDRESS) + self.assert_length(mail.outbox, 1) + self.assertEqual(self.email_envelope_from(mail.outbox[0]), settings.NOREPLY_EMAIL_ADDRESS) self.assertRegex( - self.email_display_from(outbox[0]), + self.email_display_from(mail.outbox[0]), rf"^testserver account security <{self.TOKENIZED_NOREPLY_REGEX}>\Z", ) - self.assertIn("Reactivate your Zulip organization", outbox[0].subject) - self.assertIn("Dear former administrators", outbox[0].body) + self.assertIn("Reactivate your Zulip organization", mail.outbox[0].subject) + self.assertIn("Dear former administrators", mail.outbox[0].body) admins = realm.get_human_admin_users() confirmation_url = self.get_confirmation_url_from_outbox(admins[0].delivery_email) response = self.client_get(confirmation_url) diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py index 990133e646..9766a4dea4 100644 --- a/zerver/tests/test_signup.py +++ b/zerver/tests/test_signup.py @@ -557,7 +557,10 @@ class PasswordResetTest(ZulipTestCase): user_profile = self.example_user("hamlet") email = user_profile.delivery_email do_deactivate_realm( - user_profile.realm, acting_user=None, deactivation_reason="owner_request" + user_profile.realm, + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) # start the password reset process by supplying an email address @@ -4353,7 +4356,10 @@ class TestFindMyTeam(ZulipTestCase): def test_find_team_deactivated_realm(self) -> None: do_deactivate_realm( - get_realm("zulip"), acting_user=None, deactivation_reason="owner_request" + get_realm("zulip"), + acting_user=None, + deactivation_reason="owner_request", + email_owners=False, ) data = {"emails": self.example_email("hamlet")} result = self.client_post("/accounts/find/", data) diff --git a/zerver/views/realm.py b/zerver/views/realm.py index 1945735fa0..e2e1fe4462 100644 --- a/zerver/views/realm.py +++ b/zerver/views/realm.py @@ -494,7 +494,9 @@ def update_realm( @has_request_variables def deactivate_realm(request: HttpRequest, user: UserProfile) -> HttpResponse: realm = user.realm - do_deactivate_realm(realm, acting_user=user, deactivation_reason="owner_request") + do_deactivate_realm( + realm, acting_user=user, deactivation_reason="owner_request", email_owners=True + ) return json_success(request) diff --git a/zproject/jinja2/__init__.py b/zproject/jinja2/__init__.py index 041a31c8df..232a44e675 100644 --- a/zproject/jinja2/__init__.py +++ b/zproject/jinja2/__init__.py @@ -5,6 +5,7 @@ from django.contrib.staticfiles.storage import staticfiles_storage from django.template.defaultfilters import pluralize, slugify from django.urls import reverse from django.utils import translation +from django.utils.formats import localize from django.utils.timesince import timesince from jinja2 import Environment from two_factor.plugins.phonenumber.templatetags.phonenumber import device_action @@ -39,6 +40,7 @@ def environment(**options: Any) -> Environment: env.filters["display_list"] = display_list env.filters["device_action"] = device_action env.filters["timesince"] = timesince + env.filters["localize"] = localize env.policies["json.dumps_function"] = json_dumps env.policies["json.dumps_kwargs"] = {}