mirror of https://github.com/zulip/zulip.git
user_groups: Add API support for deactivating user groups.
This commit is contained in:
parent
bef7cfe00f
commit
e1cfe61452
|
@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with.
|
||||||
|
|
||||||
## Changes in Zulip 10.0
|
## 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**
|
**Feature level 289**
|
||||||
|
|
||||||
* [`POST /users/{user_id}/subscription`](/api/subscribe): In the response,
|
* [`POST /users/{user_id}/subscription`](/api/subscribe): In the response,
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
* [Create a user group](/api/create-user-group)
|
* [Create a user group](/api/create-user-group)
|
||||||
* [Update a user group](/api/update-user-group)
|
* [Update a user group](/api/update-user-group)
|
||||||
* [Delete a user group](/api/remove-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 user group members](/api/update-user-group-members)
|
||||||
* [Update subgroups of a user group](/api/update-user-group-subgroups)
|
* [Update subgroups of a user group](/api/update-user-group-subgroups)
|
||||||
* [Get user group membership status](/api/get-is-user-group-member)
|
* [Get user group membership status](/api/get-is-user-group-member)
|
||||||
|
|
|
@ -815,6 +815,7 @@ exports.fixtures = {
|
||||||
direct_subgroup_ids: [2],
|
direct_subgroup_ids: [2],
|
||||||
can_manage_group: 16,
|
can_manage_group: 16,
|
||||||
can_mention_group: 11,
|
can_mention_group: 11,
|
||||||
|
deactivated: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -182,6 +182,7 @@ def do_send_create_user_group_event(
|
||||||
direct_subgroup_ids=[direct_subgroup.id for direct_subgroup in direct_subgroups],
|
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_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),
|
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))
|
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)
|
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)
|
@transaction.atomic(savepoint=False)
|
||||||
def do_change_user_group_permission_setting(
|
def do_change_user_group_permission_setting(
|
||||||
user_group: NamedUserGroup,
|
user_group: NamedUserGroup,
|
||||||
|
|
|
@ -1817,6 +1817,7 @@ group_type = DictType(
|
||||||
("is_system_group", bool),
|
("is_system_group", bool),
|
||||||
("can_manage_group", group_setting_type),
|
("can_manage_group", group_setting_type),
|
||||||
("can_mention_group", group_setting_type),
|
("can_mention_group", group_setting_type),
|
||||||
|
("deactivated", bool),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1865,6 +1866,7 @@ user_group_data_type = DictType(
|
||||||
("description", str),
|
("description", str),
|
||||||
("can_manage_group", group_setting_type),
|
("can_manage_group", group_setting_type),
|
||||||
("can_mention_group", group_setting_type),
|
("can_mention_group", group_setting_type),
|
||||||
|
("deactivated", bool),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ from typing import TypedDict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import connection, transaction
|
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.timezone import now as timezone_now
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django_cte import With
|
from django_cte import With
|
||||||
|
@ -53,6 +53,7 @@ class UserGroupDict(TypedDict):
|
||||||
is_system_group: bool
|
is_system_group: bool
|
||||||
can_manage_group: int | AnonymousSettingGroupDict
|
can_manage_group: int | AnonymousSettingGroupDict
|
||||||
can_mention_group: int | AnonymousSettingGroupDict
|
can_mention_group: int | AnonymousSettingGroupDict
|
||||||
|
deactivated: bool
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -126,6 +127,70 @@ def access_user_group_by_id(
|
||||||
return user_group
|
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
|
@contextmanager
|
||||||
def lock_subgroups_with_respect_to_supergroup(
|
def lock_subgroups_with_respect_to_supergroup(
|
||||||
potential_subgroup_ids: Collection[int], potential_supergroup_id: int, acting_user: UserProfile
|
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(
|
can_mention_group=get_setting_value_for_user_group_object(
|
||||||
user_group.can_mention_group, group_members, group_subgroups
|
user_group.can_mention_group, group_members, group_subgroups
|
||||||
),
|
),
|
||||||
|
deactivated=user_group.deactivated,
|
||||||
)
|
)
|
||||||
|
|
||||||
for group_dict in group_dicts.values():
|
for group_dict in group_dicts.values():
|
||||||
|
|
|
@ -106,6 +106,7 @@ class AuditLogEventType(IntEnum):
|
||||||
USER_GROUP_NAME_CHANGED = 720
|
USER_GROUP_NAME_CHANGED = 720
|
||||||
USER_GROUP_DESCRIPTION_CHANGED = 721
|
USER_GROUP_DESCRIPTION_CHANGED = 721
|
||||||
USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722
|
USER_GROUP_GROUP_BASED_SETTING_CHANGED = 722
|
||||||
|
USER_GROUP_DEACTIVATED = 723
|
||||||
|
|
||||||
# The following values are only for remote server/realm logs.
|
# The following values are only for remote server/realm logs.
|
||||||
# Values should be exactly 10000 greater than the corresponding
|
# Values should be exactly 10000 greater than the corresponding
|
||||||
|
|
|
@ -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"])
|
@openapi_param_value_generator(["/realm/filters/{filter_id}:delete"])
|
||||||
def remove_realm_filters() -> dict[str, object]:
|
def remove_realm_filters() -> dict[str, object]:
|
||||||
filter_id = do_add_linkifier(
|
filter_id = do_add_linkifier(
|
||||||
|
|
|
@ -3228,6 +3228,14 @@ paths:
|
||||||
[setting-values]: /api/group-setting-values
|
[setting-values]: /api/group-setting-values
|
||||||
[system-groups]: /api/group-setting-values#system-groups
|
[system-groups]: /api/group-setting-values#system-groups
|
||||||
[mentions]: /help/mention-a-user-or-group
|
[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:
|
example:
|
||||||
{
|
{
|
||||||
"type": "user_group",
|
"type": "user_group",
|
||||||
|
@ -20048,6 +20056,14 @@ paths:
|
||||||
[setting-values]: /api/group-setting-values
|
[setting-values]: /api/group-setting-values
|
||||||
[system-groups]: /api/group-setting-values#system-groups
|
[system-groups]: /api/group-setting-values#system-groups
|
||||||
[mentions]: /help/mention-a-user-or-group
|
[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: |
|
description: |
|
||||||
A list of `user_group` objects.
|
A list of `user_group` objects.
|
||||||
example:
|
example:
|
||||||
|
@ -20211,6 +20227,27 @@ paths:
|
||||||
"result": "success",
|
"result": "success",
|
||||||
"is_user_group_member": false,
|
"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:
|
/real-time:
|
||||||
# This entry is a hack; it exists to give us a place to put the text
|
# 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.
|
# documenting the parameters for call_on_each_event and friends.
|
||||||
|
@ -21268,6 +21305,14 @@ components:
|
||||||
[setting-values]: /api/group-setting-values
|
[setting-values]: /api/group-setting-values
|
||||||
[system-groups]: /api/group-setting-values#system-groups
|
[system-groups]: /api/group-setting-values#system-groups
|
||||||
[mentions]: /help/mention-a-user-or-group
|
[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:
|
GroupSettingValue:
|
||||||
oneOf:
|
oneOf:
|
||||||
- type: integer
|
- type: integer
|
||||||
|
|
|
@ -52,6 +52,7 @@ from zerver.actions.user_groups import (
|
||||||
bulk_remove_members_from_user_groups,
|
bulk_remove_members_from_user_groups,
|
||||||
check_add_user_group,
|
check_add_user_group,
|
||||||
do_change_user_group_permission_setting,
|
do_change_user_group_permission_setting,
|
||||||
|
do_deactivate_user_group,
|
||||||
do_update_user_group_description,
|
do_update_user_group_description,
|
||||||
do_update_user_group_name,
|
do_update_user_group_name,
|
||||||
remove_subgroups_from_user_group,
|
remove_subgroups_from_user_group,
|
||||||
|
@ -1454,3 +1455,25 @@ class TestRealmAuditLog(ZulipTestCase):
|
||||||
"property": "can_mention_group",
|
"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)
|
||||||
|
|
|
@ -111,6 +111,7 @@ from zerver.actions.user_groups import (
|
||||||
check_add_user_group,
|
check_add_user_group,
|
||||||
check_delete_user_group,
|
check_delete_user_group,
|
||||||
do_change_user_group_permission_setting,
|
do_change_user_group_permission_setting,
|
||||||
|
do_deactivate_user_group,
|
||||||
do_update_user_group_description,
|
do_update_user_group_description,
|
||||||
do_update_user_group_name,
|
do_update_user_group_name,
|
||||||
remove_subgroups_from_user_group,
|
remove_subgroups_from_user_group,
|
||||||
|
@ -1920,6 +1921,11 @@ class NormalActionsTest(BaseAction):
|
||||||
remove_subgroups_from_user_group(backend, [api_design], acting_user=None)
|
remove_subgroups_from_user_group(backend, [api_design], acting_user=None)
|
||||||
check_user_group_remove_subgroups("events[0]", events[0])
|
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
|
# Test remove event
|
||||||
with self.verify_action() as events:
|
with self.verify_action() as events:
|
||||||
check_delete_user_group(backend, acting_user=othello)
|
check_delete_user_group(backend, acting_user=othello)
|
||||||
|
|
|
@ -8,7 +8,15 @@ from django.db import transaction
|
||||||
from django.utils.timezone import now as timezone_now
|
from django.utils.timezone import now as timezone_now
|
||||||
|
|
||||||
from zerver.actions.create_realm import do_create_realm
|
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 (
|
from zerver.actions.user_groups import (
|
||||||
add_subgroups_to_user_group,
|
add_subgroups_to_user_group,
|
||||||
bulk_add_members_to_user_groups,
|
bulk_add_members_to_user_groups,
|
||||||
|
@ -16,6 +24,7 @@ from zerver.actions.user_groups import (
|
||||||
check_add_user_group,
|
check_add_user_group,
|
||||||
create_user_group_in_database,
|
create_user_group_in_database,
|
||||||
do_change_user_group_permission_setting,
|
do_change_user_group_permission_setting,
|
||||||
|
do_deactivate_user_group,
|
||||||
promote_new_full_members,
|
promote_new_full_members,
|
||||||
)
|
)
|
||||||
from zerver.actions.users import do_deactivate_user
|
from zerver.actions.users import do_deactivate_user
|
||||||
|
@ -43,6 +52,7 @@ from zerver.models import (
|
||||||
GroupGroupMembership,
|
GroupGroupMembership,
|
||||||
NamedUserGroup,
|
NamedUserGroup,
|
||||||
Realm,
|
Realm,
|
||||||
|
Stream,
|
||||||
UserGroup,
|
UserGroup,
|
||||||
UserGroupMembership,
|
UserGroupMembership,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
|
@ -83,6 +93,7 @@ class UserGroupTestCase(ZulipTestCase):
|
||||||
self.assertEqual(user_groups[0]["direct_subgroup_ids"], [])
|
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_manage_group"], user_group.id)
|
||||||
self.assertEqual(user_groups[0]["can_mention_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)
|
owners_system_group = NamedUserGroup.objects.get(name=SystemGroups.OWNERS, realm=realm)
|
||||||
membership = UserGroupMembership.objects.filter(user_group=owners_system_group).values_list(
|
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]["direct_subgroup_ids"], [])
|
||||||
self.assertEqual(user_groups[1]["can_manage_group"], user_group.id)
|
self.assertEqual(user_groups[1]["can_manage_group"], user_group.id)
|
||||||
self.assertEqual(user_groups[1]["can_mention_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(
|
admins_system_group = NamedUserGroup.objects.get(
|
||||||
name=SystemGroups.ADMINISTRATORS, realm=realm
|
name=SystemGroups.ADMINISTRATORS, realm=realm
|
||||||
|
@ -112,6 +124,7 @@ class UserGroupTestCase(ZulipTestCase):
|
||||||
self.assertEqual(user_groups[9]["members"], [])
|
self.assertEqual(user_groups[9]["members"], [])
|
||||||
self.assertEqual(user_groups[9]["can_manage_group"], user_group.id)
|
self.assertEqual(user_groups[9]["can_manage_group"], user_group.id)
|
||||||
self.assertEqual(user_groups[9]["can_mention_group"], everyone_group.id)
|
self.assertEqual(user_groups[9]["can_mention_group"], everyone_group.id)
|
||||||
|
self.assertFalse(user_groups[0]["deactivated"])
|
||||||
|
|
||||||
othello = self.example_user("othello")
|
othello = self.example_user("othello")
|
||||||
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
|
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
|
||||||
|
@ -147,6 +160,12 @@ class UserGroupTestCase(ZulipTestCase):
|
||||||
user_groups[10]["can_mention_group"].direct_subgroups,
|
user_groups[10]["can_mention_group"].direct_subgroups,
|
||||||
[admins_system_group.id, hamletcharacters_group.id],
|
[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:
|
def test_get_direct_user_groups(self) -> None:
|
||||||
othello = self.example_user("othello")
|
othello = self.example_user("othello")
|
||||||
|
@ -1149,6 +1168,238 @@ class UserGroupAPITestCase(UserGroupTestCase):
|
||||||
result = self.client_delete(f"/json/user_groups/{lear_test_group.id}")
|
result = self.client_delete(f"/json/user_groups/{lear_test_group.id}")
|
||||||
self.assert_json_error(result, "Invalid user group")
|
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:
|
def test_query_counts(self) -> None:
|
||||||
hamlet = self.example_user("hamlet")
|
hamlet = self.example_user("hamlet")
|
||||||
cordelia = self.example_user("cordelia")
|
cordelia = self.example_user("cordelia")
|
||||||
|
|
|
@ -13,6 +13,7 @@ from zerver.actions.user_groups import (
|
||||||
check_add_user_group,
|
check_add_user_group,
|
||||||
check_delete_user_group,
|
check_delete_user_group,
|
||||||
do_change_user_group_permission_setting,
|
do_change_user_group_permission_setting,
|
||||||
|
do_deactivate_user_group,
|
||||||
do_update_user_group_description,
|
do_update_user_group_description,
|
||||||
do_update_user_group_name,
|
do_update_user_group_name,
|
||||||
remove_subgroups_from_user_group,
|
remove_subgroups_from_user_group,
|
||||||
|
@ -26,6 +27,7 @@ from zerver.lib.user_groups import (
|
||||||
AnonymousSettingGroupDict,
|
AnonymousSettingGroupDict,
|
||||||
GroupSettingChangeRequest,
|
GroupSettingChangeRequest,
|
||||||
access_user_group_by_id,
|
access_user_group_by_id,
|
||||||
|
access_user_group_for_deactivation,
|
||||||
access_user_group_for_setting,
|
access_user_group_for_setting,
|
||||||
check_user_group_name,
|
check_user_group_name,
|
||||||
get_direct_memberships_of_users,
|
get_direct_memberships_of_users,
|
||||||
|
@ -183,6 +185,19 @@ def delete_user_group(
|
||||||
return json_success(request)
|
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
|
@require_member_or_admin
|
||||||
@typed_endpoint
|
@typed_endpoint
|
||||||
def update_user_group_backend(
|
def update_user_group_backend(
|
||||||
|
|
|
@ -188,6 +188,7 @@ from zerver.views.upload import (
|
||||||
)
|
)
|
||||||
from zerver.views.user_groups import (
|
from zerver.views.user_groups import (
|
||||||
add_user_group,
|
add_user_group,
|
||||||
|
deactivate_user_group,
|
||||||
delete_user_group,
|
delete_user_group,
|
||||||
edit_user_group,
|
edit_user_group,
|
||||||
get_is_user_group_member,
|
get_is_user_group_member,
|
||||||
|
@ -413,6 +414,7 @@ v1_api_and_json_patterns = [
|
||||||
rest_path(
|
rest_path(
|
||||||
"user_groups/<int:user_group_id>/members/<int:user_id>", GET=get_is_user_group_member
|
"user_groups/<int:user_group_id>/members/<int:user_id>", GET=get_is_user_group_member
|
||||||
),
|
),
|
||||||
|
rest_path("user_groups/<int:user_group_id>/deactivate", POST=deactivate_user_group),
|
||||||
# users/me -> zerver.views.user_settings
|
# users/me -> zerver.views.user_settings
|
||||||
rest_path("users/me/avatar", POST=set_avatar_backend, DELETE=delete_avatar_backend),
|
rest_path("users/me/avatar", POST=set_avatar_backend, DELETE=delete_avatar_backend),
|
||||||
# users/me/onboarding_steps -> zerver.views.onboarding_steps
|
# users/me/onboarding_steps -> zerver.views.onboarding_steps
|
||||||
|
|
Loading…
Reference in New Issue