diff --git a/templates/zerver/api/changelog.md b/templates/zerver/api/changelog.md index 3cc4c28c51..5fdbb07964 100644 --- a/templates/zerver/api/changelog.md +++ b/templates/zerver/api/changelog.md @@ -36,6 +36,8 @@ format used by the Zulip server that they are interacting with. given user group. * [`GET /user_groups/{user_group_id}/members`](/api/get-user-group-members): Added new endpoint to get members of a user group. +* [`GET /user_groups/{user_group_id}/members`](/api/get-user-group-subgroups): + Added new endpoint to get subgroups of a user group. **Feature level 126** diff --git a/templates/zerver/help/include/rest-endpoints.md b/templates/zerver/help/include/rest-endpoints.md index b09318492b..e803234ca8 100644 --- a/templates/zerver/help/include/rest-endpoints.md +++ b/templates/zerver/help/include/rest-endpoints.md @@ -64,6 +64,7 @@ * [Update user group subgroups](/api/update-user-group-subgroups) * [Get user group membership status](/api/get-is-user-group-member) * [Get user group members](/api/get-user-group-members) +* [Get subgroups of user group](/api/get-user-group-subgroups) * [Mute a user](/api/mute-user) * [Unmute a user](/api/unmute-user) diff --git a/version.py b/version.py index db8f1a0a18..5c8d8698f0 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3" # Changes should be accompanied by documentation explaining what the # new level means in templates/zerver/api/changelog.md, as well as # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. -API_FEATURE_LEVEL = 126 +API_FEATURE_LEVEL = 127 # Bump the minor PROVISION_VERSION to indicate that folks should provision # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/lib/user_groups.py b/zerver/lib/user_groups.py index 97b69f7ea0..a287d66f25 100644 --- a/zerver/lib/user_groups.py +++ b/zerver/lib/user_groups.py @@ -179,6 +179,19 @@ def get_user_group_member_ids( return list(member_ids) +def get_subgroup_ids(user_group: UserGroup, *, direct_subgroup_only: bool = False) -> List[int]: + if direct_subgroup_only: + subgroup_ids = user_group.direct_subgroups.all().values_list("id", flat=True) + else: + subgroup_ids = ( + get_recursive_subgroups(user_group) + .exclude(id=user_group.id) + .values_list("id", flat=True) + ) + + return list(subgroup_ids) + + def create_system_user_groups_for_realm(realm: Realm) -> Dict[int, UserGroup]: """Any changes to this function likely require a migration to adjust existing realms. See e.g. migration 0375_create_role_based_system_groups.py, diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 06d4469e5e..40b368fd6d 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -14009,6 +14009,49 @@ paths: responses: "200": $ref: "#/components/responses/SimpleSuccess" + get: + operationId: get-user-group-subgroups + summary: Get subgroups of the user group + tags: ["users"] + description: | + Get the subgroups of a [user group](/help/user-groups). + + `GET {{ api_url }}/v1/user_groups/{user_group_id}/subgroups` + + **Changes**: New in Zulip 6.0 (feature level 127). + parameters: + - $ref: "#/components/parameters/UserGroupId" + - name: direct_subgroup_only + in: query + description: | + Whether to consider only direct subgroups of the user group + or subgroups of subgroups also. + schema: + type: boolean + default: false + example: true + required: false + responses: + "200": + description: Success + content: + application/json: + schema: + allOf: + - $ref: "#/components/schemas/JsonSuccessBase" + - $ref: "#/components/schemas/SuccessDescription" + - additionalProperties: false + properties: + result: {} + msg: {} + subgroups: + type: array + items: + type: integer + description: | + A list containing the IDs of subgroups of the user group. + example: + {"msg": "", "result": "success", "subgroups": [2, 3]} /user_groups/{user_group_id}/members/{user_id}: get: operationId: get-is-user-group-member diff --git a/zerver/tests/test_user_groups.py b/zerver/tests/test_user_groups.py index 7704e32f48..f6b6a7b111 100644 --- a/zerver/tests/test_user_groups.py +++ b/zerver/tests/test_user_groups.py @@ -979,3 +979,46 @@ class UserGroupAPITestCase(UserGroupTestCase): self.client_get(f"/json/user_groups/{moderators_group.id}/members", info=params).content ) self.assertCountEqual(result_dict["members"], [shiva.id]) + + def test_get_subgroups_of_user_group(self) -> None: + realm = get_realm("zulip") + owners_group = UserGroup.objects.get(name="@role:owners", realm=realm, is_system_group=True) + admins_group = UserGroup.objects.get( + name="@role:administrators", realm=realm, is_system_group=True + ) + moderators_group = UserGroup.objects.get( + name="@role:moderators", realm=realm, is_system_group=True + ) + self.login("iago") + + # Test invalid user group id + result = self.client_get("/json/user_groups/25/subgroups") + self.assert_json_error(result, "Invalid user group") + + result_dict = orjson.loads( + self.client_get(f"/json/user_groups/{moderators_group.id}/subgroups").content + ) + self.assertEqual(result_dict["subgroups"], [admins_group.id, owners_group.id]) + + params = {"direct_subgroup_only": orjson.dumps(True).decode()} + result_dict = orjson.loads( + self.client_get( + f"/json/user_groups/{moderators_group.id}/subgroups", info=params + ).content + ) + self.assertCountEqual(result_dict["subgroups"], [admins_group.id]) + + # User not part of a group can also get its subgroups. + self.login("hamlet") + result_dict = orjson.loads( + self.client_get(f"/json/user_groups/{moderators_group.id}/subgroups").content + ) + self.assertEqual(result_dict["subgroups"], [admins_group.id, owners_group.id]) + + params = {"direct_subgroup_only": orjson.dumps(True).decode()} + result_dict = orjson.loads( + self.client_get( + f"/json/user_groups/{moderators_group.id}/subgroups", info=params + ).content + ) + self.assertCountEqual(result_dict["subgroups"], [admins_group.id]) diff --git a/zerver/views/user_groups.py b/zerver/views/user_groups.py index f46de916c1..e9e4afe07d 100644 --- a/zerver/views/user_groups.py +++ b/zerver/views/user_groups.py @@ -21,6 +21,7 @@ from zerver.lib.user_groups import ( access_user_group_by_id, access_user_groups_as_potential_subgroups, get_direct_memberships_of_users, + get_subgroup_ids, get_user_group_direct_members, get_user_group_member_ids, is_user_in_group, @@ -259,3 +260,19 @@ def get_user_group_members( "members": get_user_group_member_ids(user_group, direct_member_only=direct_member_only) }, ) + + +@require_member_or_admin +@has_request_variables +def get_subgroups_of_user_group( + request: HttpRequest, + user_profile: UserProfile, + user_group_id: int = REQ(json_validator=check_int, path_only=True), + direct_subgroup_only: bool = REQ(json_validator=check_bool, default=False), +) -> HttpResponse: + user_group = access_user_group_by_id(user_group_id, user_profile, for_read=True) + + return json_success( + request, + data={"subgroups": get_subgroup_ids(user_group, direct_subgroup_only=direct_subgroup_only)}, + ) diff --git a/zproject/urls.py b/zproject/urls.py index 5dc185ff76..bc245ed4c7 100644 --- a/zproject/urls.py +++ b/zproject/urls.py @@ -177,6 +177,7 @@ from zerver.views.user_groups import ( delete_user_group, edit_user_group, get_is_user_group_member, + get_subgroups_of_user_group, get_user_group, get_user_group_members, update_subgroups_of_user_group, @@ -379,7 +380,11 @@ v1_api_and_json_patterns = [ GET=get_user_group_members, POST=update_user_group_backend, ), - rest_path("user_groups//subgroups", POST=update_subgroups_of_user_group), + rest_path( + "user_groups//subgroups", + POST=update_subgroups_of_user_group, + GET=get_subgroups_of_user_group, + ), rest_path( "user_groups//members/", GET=get_is_user_group_member ),