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"] = {}