diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index f690aa4f12..be8aa20a95 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -26,6 +26,9 @@ format used by the Zulip server that they are interacting with. /register`](/api/register-queue): Added `subgroups` field, which is a list of IDs of all the subgroups of the user group, to user group objects. +* [`GET /events`](/api/get-events): Added new `user_group` events + operations for live updates to subgroups (`add_subgroups` and + `remove_subgroups`). **Feature level 126** diff --git a/zerver/actions/user_groups.py b/zerver/actions/user_groups.py index 2aedb106be..c417856603 100644 --- a/zerver/actions/user_groups.py +++ b/zerver/actions/user_groups.py @@ -8,7 +8,14 @@ from django.utils.translation import gettext as _ from zerver.lib.exceptions import JsonableError from zerver.lib.user_groups import access_user_group_by_id, create_user_group -from zerver.models import Realm, UserGroup, UserGroupMembership, UserProfile, active_user_ids +from zerver.models import ( + GroupGroupMembership, + Realm, + UserGroup, + UserGroupMembership, + UserProfile, + active_user_ids, +) from zerver.tornado.django_api import send_event @@ -158,6 +165,36 @@ def remove_members_from_user_group(user_group: UserGroup, user_profile_ids: List do_send_user_group_members_update_event("remove_members", user_group, user_profile_ids) +def do_send_subgroups_update_event( + event_name: str, user_group: UserGroup, subgroup_ids: List[int] +) -> None: + event = dict( + type="user_group", op=event_name, group_id=user_group.id, subgroup_ids=subgroup_ids + ) + transaction.on_commit( + lambda: send_event(user_group.realm, event, active_user_ids(user_group.realm_id)) + ) + + +@transaction.atomic +def add_subgroups_to_user_group(user_group: UserGroup, subgroups: List[UserGroup]) -> None: + group_memberships = [ + GroupGroupMembership(supergroup=user_group, subgroup=subgroup) for subgroup in subgroups + ] + GroupGroupMembership.objects.bulk_create(group_memberships) + + subgroup_ids = [subgroup.id for subgroup in subgroups] + do_send_subgroups_update_event("add_subgroups", user_group, subgroup_ids) + + +@transaction.atomic +def remove_subgroups_from_user_group(user_group: UserGroup, subgroups: List[UserGroup]) -> None: + GroupGroupMembership.objects.filter(supergroup=user_group, subgroup__in=subgroups).delete() + + subgroup_ids = [subgroup.id for subgroup in subgroups] + do_send_subgroups_update_event("remove_subgroups", user_group, subgroup_ids) + + def do_send_delete_user_group_event(realm: Realm, user_group_id: int, realm_id: int) -> None: event = dict(type="user_group", op="remove", group_id=user_group_id) send_event(realm, event, active_user_ids(realm_id)) diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index 1e98865a78..eea01c47ef 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -1748,6 +1748,28 @@ def check_user_group_update(var_name: str, event: Dict[str, object], field: str) assert set(event["data"].keys()) == {field} +user_group_add_subgroups_event = event_dict_type( + required_keys=[ + ("type", Equals("user_group")), + ("op", Equals("add_subgroups")), + ("group_id", int), + ("subgroup_ids", ListType(int)), + ] +) +check_user_group_add_subgroups = make_checker(user_group_add_subgroups_event) + + +user_group_remove_subgroups_event = event_dict_type( + required_keys=[ + ("type", Equals("user_group")), + ("op", Equals("remove_subgroups")), + ("group_id", int), + ("subgroup_ids", ListType(int)), + ] +) +check_user_group_remove_subgroups = make_checker(user_group_remove_subgroups_event) + + user_status_event = event_dict_type( required_keys=[ # force vertical diff --git a/zerver/lib/events.py b/zerver/lib/events.py index b26c7cff70..fdbffee019 100644 --- a/zerver/lib/events.py +++ b/zerver/lib/events.py @@ -1245,6 +1245,17 @@ def apply_event( members = set(user_group["members"]) user_group["members"] = list(members - set(event["user_ids"])) user_group["members"].sort() + elif event["op"] == "add_subgroups": + for user_group in state["realm_user_groups"]: + if user_group["id"] == event["group_id"]: + user_group["subgroups"].extend(event["subgroup_ids"]) + user_group["subgroups"].sort() + elif event["op"] == "remove_subgroups": + for user_group in state["realm_user_groups"]: + if user_group["id"] == event["group_id"]: + subgroups = set(user_group["subgroups"]) + user_group["subgroups"] = list(subgroups - set(event["subgroup_ids"])) + user_group["subgroups"].sort() elif event["op"] == "remove": state["realm_user_groups"] = [ ug for ug in state["realm_user_groups"] if ug["id"] != event["group_id"] diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 1c61a21900..7998f53081 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -2830,6 +2830,82 @@ paths: "user_ids": [10], "id": 0, } + - type: object + additionalProperties: false + description: | + Event sent to all users when subgroups have been added to + a user group. + + **Changes**: New in Zulip 6.0 (feature level 127). + properties: + id: + $ref: "#/components/schemas/EventIdSchema" + type: + allOf: + - $ref: "#/components/schemas/EventTypeSchema" + - enum: + - user_group + op: + type: string + enum: + - add_subgroups + group_id: + type: integer + description: | + The ID of the user group whose details have changed. + subgroup_ids: + type: array + items: + type: integer + description: | + Array containing the IDs of the subgroups that have been added + to the user group. + example: + { + "type": "user_group", + "op": "add_subgroups", + "group_id": 2, + "subgroup_ids": [10], + "id": 0, + } + - type: object + additionalProperties: false + description: | + Event sent to all users when subgroups have been removed from + a user group. + + **Changes**: New in Zulip 6.0 (feature level 127). + properties: + id: + $ref: "#/components/schemas/EventIdSchema" + type: + allOf: + - $ref: "#/components/schemas/EventTypeSchema" + - enum: + - user_group + op: + type: string + enum: + - remove_subgroups + group_id: + type: integer + description: | + The ID of the user group whose details have changed. + subgroup_ids: + type: array + items: + type: integer + description: | + Array containing the IDs of the subgroups that have been + removed from the user group. + example: + { + "type": "user_group", + "op": "remove_subgroups", + "group_id": 2, + "subgroup_ids": [10], + "id": 0, + } - type: object additionalProperties: false description: | diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index 6c6d6dd6c9..52e7725923 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -92,12 +92,14 @@ from zerver.actions.streams import ( from zerver.actions.submessage import do_add_submessage from zerver.actions.typing import check_send_typing_notification, do_send_stream_typing_notification from zerver.actions.user_groups import ( + add_subgroups_to_user_group, bulk_add_members_to_user_group, check_add_user_group, check_delete_user_group, do_update_user_group_description, do_update_user_group_name, remove_members_from_user_group, + remove_subgroups_from_user_group, ) from zerver.actions.user_settings import ( do_change_avatar_fields, @@ -171,8 +173,10 @@ from zerver.lib.event_schema import ( check_update_message_flags_remove, check_user_group_add, check_user_group_add_members, + check_user_group_add_subgroups, check_user_group_remove, check_user_group_remove_members, + check_user_group_remove_subgroups, check_user_group_update, check_user_settings_update, check_user_status, @@ -194,6 +198,7 @@ from zerver.lib.test_helpers import ( stdout_suppressed, ) from zerver.lib.topic import TOPIC_NAME +from zerver.lib.user_groups import create_user_group from zerver.lib.user_mutes import get_mute_object from zerver.models import ( Attachment, @@ -1225,6 +1230,18 @@ class NormalActionsTest(BaseAction): events = self.verify_action(lambda: remove_members_from_user_group(backend, [hamlet.id])) check_user_group_remove_members("events[0]", events[0]) + api_design = create_user_group( + "api-design", [hamlet], hamlet.realm, description="API design team" + ) + + # Test add subgroups + events = self.verify_action(lambda: add_subgroups_to_user_group(backend, [api_design])) + check_user_group_add_subgroups("events[0]", events[0]) + + # Test remove subgroups + events = self.verify_action(lambda: remove_subgroups_from_user_group(backend, [api_design])) + check_user_group_remove_subgroups("events[0]", events[0]) + # Test remove event events = self.verify_action(lambda: check_delete_user_group(backend.id, othello)) check_user_group_remove("events[0]", events[0])