From e1cfe61452417cd84cc1dd3ffc5a9eb87e4a044d Mon Sep 17 00:00:00 2001 From: Sahil Batra Date: Wed, 15 May 2024 19:14:18 +0530 Subject: [PATCH] user_groups: Add API support for deactivating user groups. --- api_docs/changelog.md | 10 + api_docs/include/rest-endpoints.md | 1 + web/tests/lib/events.js | 1 + zerver/actions/user_groups.py | 20 ++ zerver/lib/event_schema.py | 2 + zerver/lib/user_groups.py | 68 ++++- zerver/models/realm_audit_logs.py | 1 + zerver/openapi/curl_param_value_generators.py | 14 + zerver/openapi/zulip.yaml | 45 ++++ zerver/tests/test_audit_log.py | 23 ++ zerver/tests/test_events.py | 6 + zerver/tests/test_user_groups.py | 253 +++++++++++++++++- zerver/views/user_groups.py | 15 ++ zproject/urls.py | 2 + 14 files changed, 459 insertions(+), 2 deletions(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index f67c8da02c..424a679c50 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 10.0 +**Feature level 290** + +* [`POST /user_groups/{user_group_id}/deactivate`](/api/deactivate-user-group): + Added new API endpoint to deactivate a user group. +* [`POST /register`](/api/register-queue), [`GET + /user_groups`](/api/get-user-groups): Added `deactivated` field in + the user group objects to identify deactivated user groups. +* [`GET /events`](/api/get-events): When a user group is deactivated, + a `user_group` event with `op=update` is sent to clients. + **Feature level 289** * [`POST /users/{user_id}/subscription`](/api/subscribe): In the response, diff --git a/api_docs/include/rest-endpoints.md b/api_docs/include/rest-endpoints.md index 856655593c..8a2826269a 100644 --- a/api_docs/include/rest-endpoints.md +++ b/api_docs/include/rest-endpoints.md @@ -79,6 +79,7 @@ * [Create a user group](/api/create-user-group) * [Update a user group](/api/update-user-group) * [Delete a user group](/api/remove-user-group) +* [Deactivate a user group](/api/deactivate-user-group) * [Update user group members](/api/update-user-group-members) * [Update subgroups of a user group](/api/update-user-group-subgroups) * [Get user group membership status](/api/get-is-user-group-member) diff --git a/web/tests/lib/events.js b/web/tests/lib/events.js index 9758770a3b..0520d5e05d 100644 --- a/web/tests/lib/events.js +++ b/web/tests/lib/events.js @@ -815,6 +815,7 @@ exports.fixtures = { direct_subgroup_ids: [2], can_manage_group: 16, can_mention_group: 11, + deactivated: false, }, }, diff --git a/zerver/actions/user_groups.py b/zerver/actions/user_groups.py index 4b1221a5de..b1fe606e79 100644 --- a/zerver/actions/user_groups.py +++ b/zerver/actions/user_groups.py @@ -182,6 +182,7 @@ def do_send_create_user_group_event( direct_subgroup_ids=[direct_subgroup.id for direct_subgroup in direct_subgroups], can_manage_group=get_group_setting_value_for_api(user_group.can_manage_group), can_mention_group=get_group_setting_value_for_api(user_group.can_mention_group), + deactivated=False, ), ) send_event_on_commit(user_group.realm, event, active_user_ids(user_group.realm_id)) @@ -436,6 +437,25 @@ def check_delete_user_group(user_group: NamedUserGroup, *, acting_user: UserProf do_send_delete_user_group_event(acting_user.realm, user_group_id, acting_user.realm.id) +@transaction.atomic(savepoint=False) +def do_deactivate_user_group( + user_group: NamedUserGroup, *, acting_user: UserProfile | None +) -> None: + user_group.deactivated = True + user_group.save(update_fields=["deactivated"]) + + now = timezone_now() + RealmAuditLog.objects.create( + realm=user_group.realm, + modified_user_group_id=user_group.id, + event_type=AuditLogEventType.USER_GROUP_DEACTIVATED, + event_time=now, + acting_user=acting_user, + ) + + do_send_user_group_update_event(user_group, dict(deactivated=True)) + + @transaction.atomic(savepoint=False) def do_change_user_group_permission_setting( user_group: NamedUserGroup, diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 877af15c8f..06bab2b140 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1817,6 +1817,7 @@ group_type = DictType( ("is_system_group", bool), ("can_manage_group", group_setting_type), ("can_mention_group", group_setting_type), + ("deactivated", bool), ] ) @@ -1865,6 +1866,7 @@ user_group_data_type = DictType( ("description", str), ("can_manage_group", group_setting_type), ("can_mention_group", group_setting_type), + ("deactivated", bool), ], ) diff --git a/zerver/lib/user_groups.py b/zerver/lib/user_groups.py index f046761f69..952f542884 100644 --- a/zerver/lib/user_groups.py +++ b/zerver/lib/user_groups.py @@ -6,7 +6,7 @@ from typing import TypedDict from django.conf import settings from django.db import connection, transaction -from django.db.models import F, QuerySet +from django.db.models import F, Q, QuerySet from django.utils.timezone import now as timezone_now from django.utils.translation import gettext as _ from django_cte import With @@ -53,6 +53,7 @@ class UserGroupDict(TypedDict): is_system_group: bool can_manage_group: int | AnonymousSettingGroupDict can_mention_group: int | AnonymousSettingGroupDict + deactivated: bool @dataclass @@ -126,6 +127,70 @@ def access_user_group_by_id( return user_group +def access_user_group_for_deactivation( + user_group_id: int, user_profile: UserProfile +) -> NamedUserGroup: + user_group = access_user_group_by_id(user_group_id, user_profile, for_read=False) + + if ( + user_group.direct_supergroups.exclude(named_user_group=None) + .filter(named_user_group__deactivated=False) + .exists() + ): + raise JsonableError( + _("You cannot deactivate a user group that is subgroup of any user group.") + ) + + anonymous_supergroup_ids = user_group.direct_supergroups.filter( + named_user_group=None + ).values_list("id", flat=True) + + # We check both the cases - whether the group is being directly used + # as the value of a setting or as a subgroup of an anonymous group + # used for a setting. + setting_group_ids_using_deactivating_user_group = [ + *list(anonymous_supergroup_ids), + user_group.id, + ] + + stream_setting_query = Q() + for setting_name in Stream.stream_permission_group_settings: + stream_setting_query |= Q( + **{f"{setting_name}__in": setting_group_ids_using_deactivating_user_group} + ) + + if ( + Stream.objects.filter(realm_id=user_group.realm_id, deactivated=False) + .filter(stream_setting_query) + .exists() + ): + raise JsonableError(_("You cannot deactivate a user group which is used for setting.")) + + group_setting_query = Q() + for setting_name in NamedUserGroup.GROUP_PERMISSION_SETTINGS: + group_setting_query |= Q( + **{f"{setting_name}__in": setting_group_ids_using_deactivating_user_group} + ) + + if ( + NamedUserGroup.objects.filter(realm_id=user_group.realm_id, deactivated=False) + .filter(group_setting_query) + .exists() + ): + raise JsonableError(_("You cannot deactivate a user group which is used for setting.")) + + realm_setting_query = Q() + for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: + realm_setting_query |= Q( + **{f"{setting_name}__in": setting_group_ids_using_deactivating_user_group} + ) + + if Realm.objects.filter(id=user_group.realm_id).filter(realm_setting_query).exists(): + raise JsonableError(_("You cannot deactivate a user group which is used for setting.")) + + return user_group + + @contextmanager def lock_subgroups_with_respect_to_supergroup( potential_subgroup_ids: Collection[int], potential_supergroup_id: int, acting_user: UserProfile @@ -430,6 +495,7 @@ def user_groups_in_realm_serialized(realm: Realm) -> list[UserGroupDict]: can_mention_group=get_setting_value_for_user_group_object( user_group.can_mention_group, group_members, group_subgroups ), + deactivated=user_group.deactivated, ) for group_dict in group_dicts.values(): diff --git a/zerver/models/realm_audit_logs.py b/zerver/models/realm_audit_logs.py index 03e11f6836..426cbd1ea6 100644 --- a/zerver/models/realm_audit_logs.py +++ b/zerver/models/realm_audit_logs.py @@ -106,6 +106,7 @@ class AuditLogEventType(IntEnum): USER_GROUP_NAME_CHANGED = 720 USER_GROUP_DESCRIPTION_CHANGED = 721 USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722 + USER_GROUP_DEACTIVATED = 723 # The following values are only for remote server/realm logs. # Values should be exactly 10000 greater than the corresponding diff --git a/zerver/openapi/curl_param_value_generators.py b/zerver/openapi/curl_param_value_generators.py index 44047a1e92..a246986432 100644 --- a/zerver/openapi/curl_param_value_generators.py +++ b/zerver/openapi/curl_param_value_generators.py @@ -275,6 +275,20 @@ def get_temp_user_group_id() -> dict[str, object]: } +@openapi_param_value_generator(["/user_groups/{user_group_id}/deactivate:post"]) +def get_temp_user_group_id_for_deactivation() -> dict[str, object]: + user_group, _ = NamedUserGroup.objects.get_or_create( + name="temp-deactivation", + realm=get_realm("zulip"), + can_manage_group_id=11, + can_mention_group_id=11, + realm_for_sharding=get_realm("zulip"), + ) + return { + "user_group_id": user_group.id, + } + + @openapi_param_value_generator(["/realm/filters/{filter_id}:delete"]) def remove_realm_filters() -> dict[str, object]: filter_id = do_add_linkifier( diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index b25f9a4650..65ffd0737a 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -3228,6 +3228,14 @@ paths: [setting-values]: /api/group-setting-values [system-groups]: /api/group-setting-values#system-groups [mentions]: /help/mention-a-user-or-group + deactivated: + type: boolean + description: | + Whether the user group is deactivated. Deactivated groups + cannot be used as a subgroup of another group or used for + any other purpose. + + **Changes**: New in Zulip 10.0 (feature level 290). example: { "type": "user_group", @@ -20048,6 +20056,14 @@ paths: [setting-values]: /api/group-setting-values [system-groups]: /api/group-setting-values#system-groups [mentions]: /help/mention-a-user-or-group + deactivated: + type: boolean + description: | + Whether the user group is deactivated. Deactivated groups + cannot be used as a subgroup of another group or used for + any other purpose. + + **Changes**: New in Zulip 10.0 (feature level 290). description: | A list of `user_group` objects. example: @@ -20211,6 +20227,27 @@ paths: "result": "success", "is_user_group_member": false, } + /user_groups/{user_group_id}/deactivate: + post: + operationId: deactivate-user-group + summary: Deactivate a user group + tags: ["users"] + description: | + Deactivate a user group. Deactivated user groups cannot be + used for mentions, permissions, or any other purpose, but can + be reactivated or renamed. + + Deactivating user groups is preferable to deleting them from + the database, since the deactivation model allows audit logs + of changes to sensitive group-valued permissions to be + maintained. + + **Changes**: New in Zulip 10.0 (feature level 290). + parameters: + - $ref: "#/components/parameters/UserGroupId" + responses: + "200": + $ref: "#/components/responses/SimpleSuccess" /real-time: # This entry is a hack; it exists to give us a place to put the text # documenting the parameters for call_on_each_event and friends. @@ -21268,6 +21305,14 @@ components: [setting-values]: /api/group-setting-values [system-groups]: /api/group-setting-values#system-groups [mentions]: /help/mention-a-user-or-group + deactivated: + type: boolean + description: | + Whether the user group is deactivated. Deactivated groups + cannot be used as a subgroup of another group or used for + any other purpose. + + **Changes**: New in Zulip 10.0 (feature level 290). GroupSettingValue: oneOf: - type: integer diff --git a/zerver/tests/test_audit_log.py b/zerver/tests/test_audit_log.py index 15f91f4d11..49c99d0cfc 100644 --- a/zerver/tests/test_audit_log.py +++ b/zerver/tests/test_audit_log.py @@ -52,6 +52,7 @@ from zerver.actions.user_groups import ( bulk_remove_members_from_user_groups, check_add_user_group, do_change_user_group_permission_setting, + do_deactivate_user_group, do_update_user_group_description, do_update_user_group_name, remove_subgroups_from_user_group, @@ -1454,3 +1455,25 @@ class TestRealmAuditLog(ZulipTestCase): "property": "can_mention_group", }, ) + + def test_user_group_deactivation(self) -> None: + hamlet = self.example_user("hamlet") + cordelia = self.example_user("cordelia") + user_group = check_add_user_group( + hamlet.realm, + "test", + [hamlet, cordelia], + acting_user=hamlet, + ) + now = timezone_now() + do_deactivate_user_group(user_group, acting_user=hamlet) + + audit_log_entries = RealmAuditLog.objects.filter( + acting_user=hamlet, + realm=hamlet.realm, + event_time__gte=now, + event_type=AuditLogEventType.USER_GROUP_DEACTIVATED, + ) + self.assert_length(audit_log_entries, 1) + self.assertIsNone(audit_log_entries[0].modified_user) + self.assertEqual(audit_log_entries[0].modified_user_group, user_group) diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 0752712826..07cc5488ea 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -111,6 +111,7 @@ from zerver.actions.user_groups import ( check_add_user_group, check_delete_user_group, do_change_user_group_permission_setting, + do_deactivate_user_group, do_update_user_group_description, do_update_user_group_name, remove_subgroups_from_user_group, @@ -1920,6 +1921,11 @@ class NormalActionsTest(BaseAction): remove_subgroups_from_user_group(backend, [api_design], acting_user=None) check_user_group_remove_subgroups("events[0]", events[0]) + # Test deactivate event + with self.verify_action() as events: + do_deactivate_user_group(backend, acting_user=None) + check_user_group_update("events[0]", events[0], "deactivated") + # Test remove event with self.verify_action() as events: check_delete_user_group(backend, acting_user=othello) diff --git a/zerver/tests/test_user_groups.py b/zerver/tests/test_user_groups.py index 2d22be1d4f..95163de6ae 100644 --- a/zerver/tests/test_user_groups.py +++ b/zerver/tests/test_user_groups.py @@ -8,7 +8,15 @@ from django.db import transaction from django.utils.timezone import now as timezone_now from zerver.actions.create_realm import do_create_realm -from zerver.actions.realm_settings import do_set_realm_property +from zerver.actions.realm_settings import ( + do_change_realm_permission_group_setting, + do_set_realm_property, +) +from zerver.actions.streams import ( + do_change_stream_group_based_setting, + do_deactivate_stream, + do_unarchive_stream, +) from zerver.actions.user_groups import ( add_subgroups_to_user_group, bulk_add_members_to_user_groups, @@ -16,6 +24,7 @@ from zerver.actions.user_groups import ( check_add_user_group, create_user_group_in_database, do_change_user_group_permission_setting, + do_deactivate_user_group, promote_new_full_members, ) from zerver.actions.users import do_deactivate_user @@ -43,6 +52,7 @@ from zerver.models import ( GroupGroupMembership, NamedUserGroup, Realm, + Stream, UserGroup, UserGroupMembership, UserProfile, @@ -83,6 +93,7 @@ class UserGroupTestCase(ZulipTestCase): self.assertEqual(user_groups[0]["direct_subgroup_ids"], []) self.assertEqual(user_groups[0]["can_manage_group"], user_group.id) self.assertEqual(user_groups[0]["can_mention_group"], user_group.id) + self.assertFalse(user_groups[0]["deactivated"]) owners_system_group = NamedUserGroup.objects.get(name=SystemGroups.OWNERS, realm=realm) membership = UserGroupMembership.objects.filter(user_group=owners_system_group).values_list( @@ -95,6 +106,7 @@ class UserGroupTestCase(ZulipTestCase): self.assertEqual(user_groups[1]["direct_subgroup_ids"], []) self.assertEqual(user_groups[1]["can_manage_group"], user_group.id) self.assertEqual(user_groups[1]["can_mention_group"], user_group.id) + self.assertFalse(user_groups[0]["deactivated"]) admins_system_group = NamedUserGroup.objects.get( name=SystemGroups.ADMINISTRATORS, realm=realm @@ -112,6 +124,7 @@ class UserGroupTestCase(ZulipTestCase): self.assertEqual(user_groups[9]["members"], []) self.assertEqual(user_groups[9]["can_manage_group"], user_group.id) self.assertEqual(user_groups[9]["can_mention_group"], everyone_group.id) + self.assertFalse(user_groups[0]["deactivated"]) othello = self.example_user("othello") hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm) @@ -147,6 +160,12 @@ class UserGroupTestCase(ZulipTestCase): user_groups[10]["can_mention_group"].direct_subgroups, [admins_system_group.id, hamletcharacters_group.id], ) + self.assertFalse(user_groups[0]["deactivated"]) + + do_deactivate_user_group(new_user_group, acting_user=None) + user_groups = user_groups_in_realm_serialized(realm) + self.assertEqual(user_groups[10]["id"], new_user_group.id) + self.assertTrue(user_groups[10]["deactivated"]) def test_get_direct_user_groups(self) -> None: othello = self.example_user("othello") @@ -1149,6 +1168,238 @@ class UserGroupAPITestCase(UserGroupTestCase): result = self.client_delete(f"/json/user_groups/{lear_test_group.id}") self.assert_json_error(result, "Invalid user group") + def test_user_group_deactivation(self) -> None: + support_group = self.create_user_group_for_test("support") + leadership_group = self.create_user_group_for_test("leadership") + add_subgroups_to_user_group(support_group, [leadership_group], acting_user=None) + realm = get_realm("zulip") + + do_set_realm_property( + realm, "user_group_edit_policy", CommonPolicyEnum.ADMINS_ONLY, acting_user=None + ) + self.login("othello") + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error(result, "Insufficient permission") + + do_set_realm_property( + realm, "user_group_edit_policy", CommonPolicyEnum.MEMBERS_ONLY, acting_user=None + ) + + self.login("hamlet") + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error(result, "Insufficient permission") + + self.login("othello") + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + support_group = NamedUserGroup.objects.get(name="support", realm=realm) + self.assertTrue(support_group.deactivated) + + support_group.deactivated = False + support_group.save() + + # Check admins can deactivate groups even if they are not members + # of the group. + self.login("iago") + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + support_group = NamedUserGroup.objects.get(name="support", realm=realm) + self.assertTrue(support_group.deactivated) + + support_group.deactivated = False + support_group.save() + + # Check moderators can deactivate groups if they are allowed by + # user_group_edit_policy even when they are not members of the group. + do_set_realm_property( + realm, "user_group_edit_policy", CommonPolicyEnum.ADMINS_ONLY, acting_user=None + ) + self.login("shiva") + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error(result, "Insufficient permission") + + do_set_realm_property( + realm, "user_group_edit_policy", CommonPolicyEnum.MODERATORS_ONLY, acting_user=None + ) + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + support_group = NamedUserGroup.objects.get(name="support", realm=realm) + self.assertTrue(support_group.deactivated) + + support_group.deactivated = False + support_group.save() + + # Check that group that is subgroup of another group cannot be deactivated. + result = self.client_post(f"/json/user_groups/{leadership_group.id}/deactivate") + self.assert_json_error( + result, "You cannot deactivate a user group that is subgroup of any user group." + ) + + # If the supergroup is itself deactivated, then subgroup can be deactivated. + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + result = self.client_post(f"/json/user_groups/{leadership_group.id}/deactivate") + self.assert_json_success(result) + leadership_group = NamedUserGroup.objects.get(name="leadership", realm=realm) + self.assertTrue(leadership_group.deactivated) + + # Check that system groups cannot be deactivated at all. + self.login("desdemona") + members_system_group = NamedUserGroup.objects.get( + name=SystemGroups.MEMBERS, realm=realm, is_system_group=True + ) + result = self.client_post(f"/json/user_groups/{members_system_group.id}/deactivate") + self.assert_json_error(result, "Insufficient permission") + + def test_user_group_deactivation_with_group_used_for_settings(self) -> None: + support_group = self.create_user_group_for_test("support") + realm = get_realm("zulip") + moderators_group = NamedUserGroup.objects.get( + name=SystemGroups.MODERATORS, realm=realm, is_system_group=True + ) + hamlet = self.example_user("hamlet") + self.login("desdemona") + + for setting_name in Realm.REALM_PERMISSION_GROUP_SETTINGS: + anonymous_setting_group = self.create_or_update_anonymous_group_for_setting( + [hamlet], [moderators_group, support_group] + ) + do_change_realm_permission_group_setting( + realm, setting_name, anonymous_setting_group, acting_user=None + ) + + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error( + result, "You cannot deactivate a user group which is used for setting." + ) + + do_change_realm_permission_group_setting( + realm, setting_name, support_group, acting_user=None + ) + + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error( + result, "You cannot deactivate a user group which is used for setting." + ) + + # Reset the realm setting to one of the system group so this setting + # does not interfere when testing for another setting. + do_change_realm_permission_group_setting( + realm, setting_name, moderators_group, acting_user=None + ) + + stream = ensure_stream(realm, "support", acting_user=None) + for setting_name in Stream.stream_permission_group_settings: + do_change_stream_group_based_setting( + stream, setting_name, support_group, acting_user=None + ) + + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error( + result, "You cannot deactivate a user group which is used for setting." + ) + + # Test the group can be deactivated, if the stream which uses + # this group for a setting is deactivated. + do_deactivate_stream(stream, acting_user=None) + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + support_group = NamedUserGroup.objects.get(name="support", realm=realm) + self.assertTrue(support_group.deactivated) + + support_group.deactivated = False + support_group.save() + + do_unarchive_stream(stream, "support", acting_user=None) + + anonymous_setting_group = self.create_or_update_anonymous_group_for_setting( + [hamlet], [moderators_group, support_group] + ) + do_change_stream_group_based_setting( + stream, setting_name, anonymous_setting_group, acting_user=None + ) + + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error( + result, "You cannot deactivate a user group which is used for setting." + ) + + # Test the group can be deactivated, if the stream which uses + # this group for a setting is deactivated. + do_deactivate_stream(stream, acting_user=None) + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + support_group = NamedUserGroup.objects.get(name="support", realm=realm) + self.assertTrue(support_group.deactivated) + + # Reactivate the group again for further testing. + support_group.deactivated = False + support_group.save() + + # Reset the stream setting to one of the system group so this setting + # does not interfere when testing for another setting. + do_change_stream_group_based_setting( + stream, setting_name, moderators_group, acting_user=None + ) + + leadership_group = self.create_user_group_for_test("leadership") + for setting_name in NamedUserGroup.GROUP_PERMISSION_SETTINGS: + do_change_user_group_permission_setting( + leadership_group, setting_name, support_group, acting_user=None + ) + + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error( + result, "You cannot deactivate a user group which is used for setting." + ) + + # Test the group can be deactivated, if the user group which uses + # this group for a setting is deactivated. + do_deactivate_user_group(leadership_group, acting_user=None) + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + support_group = NamedUserGroup.objects.get(name="support", realm=realm) + self.assertTrue(support_group.deactivated) + + support_group.deactivated = False + support_group.save() + + leadership_group.deactivated = False + leadership_group.save() + + anonymous_setting_group = self.create_or_update_anonymous_group_for_setting( + [hamlet], [moderators_group, support_group] + ) + do_change_user_group_permission_setting( + leadership_group, setting_name, anonymous_setting_group, acting_user=None + ) + + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_error( + result, "You cannot deactivate a user group which is used for setting." + ) + + # Test the group can be deactivated, if the user group which uses + # this group for a setting is deactivated. + do_deactivate_user_group(leadership_group, acting_user=None) + result = self.client_post(f"/json/user_groups/{support_group.id}/deactivate") + self.assert_json_success(result) + support_group = NamedUserGroup.objects.get(name="support", realm=realm) + self.assertTrue(support_group.deactivated) + + # Reactivate the group again for further testing. + support_group.deactivated = False + support_group.save() + + leadership_group.deactivated = False + leadership_group.save() + + # Reset the group setting to one of the system group so this setting + # does not interfere when testing for another setting. + do_change_user_group_permission_setting( + leadership_group, setting_name, moderators_group, acting_user=None + ) + def test_query_counts(self) -> None: hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index 00687604d8..25f316481c 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -13,6 +13,7 @@ from zerver.actions.user_groups import ( check_add_user_group, check_delete_user_group, do_change_user_group_permission_setting, + do_deactivate_user_group, do_update_user_group_description, do_update_user_group_name, remove_subgroups_from_user_group, @@ -26,6 +27,7 @@ from zerver.lib.user_groups import ( AnonymousSettingGroupDict, GroupSettingChangeRequest, access_user_group_by_id, + access_user_group_for_deactivation, access_user_group_for_setting, check_user_group_name, get_direct_memberships_of_users, @@ -183,6 +185,19 @@ def delete_user_group( return json_success(request) +@typed_endpoint +@transaction.atomic +def deactivate_user_group( + request: HttpRequest, + user_profile: UserProfile, + *, + user_group_id: PathOnly[Json[int]], +) -> HttpResponse: + user_group = access_user_group_for_deactivation(user_group_id, user_profile) + do_deactivate_user_group(user_group, acting_user=user_profile) + return json_success(request) + + @require_member_or_admin @typed_endpoint def update_user_group_backend( diff --git a/zproject/urls.py b/zproject/urls.py index 222ebc5ba9..c65c2faefa 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -188,6 +188,7 @@ from zerver.views.upload import ( ) from zerver.views.user_groups import ( add_user_group, + deactivate_user_group, delete_user_group, edit_user_group, get_is_user_group_member, @@ -413,6 +414,7 @@ v1_api_and_json_patterns = [ rest_path( "user_groups//members/", GET=get_is_user_group_member ), + rest_path("user_groups//deactivate", POST=deactivate_user_group), # users/me -> zerver.views.user_settings rest_path("users/me/avatar", POST=set_avatar_backend, DELETE=delete_avatar_backend), # users/me/onboarding_steps -> zerver.views.onboarding_steps