groups: Allow setting anonymous group to settings while creation.

This commit adds support to set can_mention_group setting to
anonymous group while group creation.
This commit is contained in:
Sahil Batra 2024-04-29 09:21:48 +05:30 committed by Tim Abbott
parent 206014f263
commit 23fdb73353
8 changed files with 177 additions and 30 deletions

View File

@ -26,6 +26,9 @@ format used by the Zulip server that they are interacting with.
/register`](/api/register-queue): `can_mention_group` field can now /register`](/api/register-queue): `can_mention_group` field can now
either be an ID of a named user group with the permission, or an either be an ID of a named user group with the permission, or an
object describing the set of users and groups with the permission. object describing the set of users and groups with the permission.
* [`POST /user_groups/create`](/api/create-user-group): The
`can_mention_group` parameter can now either be an ID of a named
user group or an object describing a set of users and groups.
**Feature level 257**: **Feature level 257**:

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
from zerver.lib.exceptions import JsonableError from zerver.lib.exceptions import JsonableError
from zerver.lib.user_groups import ( from zerver.lib.user_groups import (
get_group_setting_value_for_api,
get_role_based_system_groups_dict, get_role_based_system_groups_dict,
set_defaults_for_group_settings, set_defaults_for_group_settings,
) )
@ -175,7 +176,7 @@ def do_send_create_user_group_event(
id=user_group.id, id=user_group.id,
is_system_group=user_group.is_system_group, is_system_group=user_group.is_system_group,
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_mention_group=user_group.can_mention_group_id, can_mention_group=get_group_setting_value_for_api(user_group.can_mention_group),
), ),
) )
send_event(user_group.realm, event, active_user_ids(user_group.realm_id)) send_event(user_group.realm, event, active_user_ids(user_group.realm_id))

View File

@ -1790,6 +1790,18 @@ update_message_flags_remove_event = event_dict_type(
check_update_message_flags_remove = make_checker(update_message_flags_remove_event) check_update_message_flags_remove = make_checker(update_message_flags_remove_event)
group_setting_type = UnionType(
[
int,
DictType(
required_keys=[
("direct_members", ListType(int)),
("direct_subgroups", ListType(int)),
]
),
]
)
group_type = DictType( group_type = DictType(
required_keys=[ required_keys=[
("id", int), ("id", int),
@ -1798,7 +1810,7 @@ group_type = DictType(
("direct_subgroup_ids", ListType(int)), ("direct_subgroup_ids", ListType(int)),
("description", str), ("description", str),
("is_system_group", bool), ("is_system_group", bool),
("can_mention_group", int), ("can_mention_group", group_setting_type),
] ]
) )

View File

@ -241,8 +241,10 @@ def update_or_create_user_group_for_setting(
direct_members: List[int], direct_members: List[int],
direct_subgroups: List[int], direct_subgroups: List[int],
current_setting_value: Optional[UserGroup], current_setting_value: Optional[UserGroup],
) -> UserGroup: # nocoverage ) -> UserGroup:
if current_setting_value is not None and not hasattr(current_setting_value, "named_user_group"): if current_setting_value is not None and not hasattr(
current_setting_value, "named_user_group"
): # nocoverage
# We do not create a new group if the setting was already set # We do not create a new group if the setting was already set
# to an anonymous group. The memberships of existing group # to an anonymous group. The memberships of existing group
# itself are updated. # itself are updated.
@ -287,16 +289,16 @@ def access_user_group_for_setting(
# The API would not allow passing the setting parameter as a Dict # The API would not allow passing the setting parameter as a Dict
# if require_system_group is true for a setting. # if require_system_group is true for a setting.
assert permission_configuration.require_system_group is False # nocoverage assert permission_configuration.require_system_group is False
user_group = update_or_create_user_group_for_setting( user_group = update_or_create_user_group_for_setting(
user_profile.realm, user_profile.realm,
setting_user_group.direct_members, setting_user_group.direct_members,
setting_user_group.direct_subgroups, setting_user_group.direct_subgroups,
current_setting_value, current_setting_value,
) # nocoverage )
return user_group # nocoverage return user_group
def check_user_group_name(group_name: str) -> str: def check_user_group_name(group_name: str) -> str:

View File

@ -18389,19 +18389,24 @@ paths:
type: integer type: integer
example: [1, 2, 3, 4] example: [1, 2, 3, 4]
can_mention_group: can_mention_group:
description: | allOf:
ID of the user group whose members are allowed to mention the new - description: |
user group. Either the ID of a named user group that has permission to
mention the group, or an object describing the set of users
and groups who have permission mention the new group.
This setting cannot be set to `"role:internet"` and `"role:owners"` This setting cannot be set to `"role:internet"` and
system groups. `"role:owners"` system groups.
**Changes**: Before Zulip 8.0 (feature level 198), **Changes**: Before Zulip 9.0 (feature level 258), the
the `can_mention_group` setting was named `can_mention_group_id`. `can_mention_group` setting could only be set to an integer.
New in Zulip 8.0 (feature level 191). Previously, groups **Changes**: Before Zulip 8.0 (feature level 198),
could be mentioned if and only if they were not system groups. the `can_mention_group` setting was named `can_mention_group_id`.
type: integer
New in Zulip 8.0 (feature level 191). Previously, groups
could be mentioned if and only if they were not system groups.
- $ref: "#/components/schemas/CanMentionGroup"
example: 11 example: 11
required: required:
- name - name

View File

@ -216,6 +216,7 @@ from zerver.lib.test_helpers import (
from zerver.lib.timestamp import convert_to_UTC, datetime_to_timestamp from zerver.lib.timestamp import convert_to_UTC, datetime_to_timestamp
from zerver.lib.topic import TOPIC_NAME from zerver.lib.topic import TOPIC_NAME
from zerver.lib.types import ProfileDataElementUpdateDict from zerver.lib.types import ProfileDataElementUpdateDict
from zerver.lib.user_groups import AnonymousSettingGroupDict
from zerver.models import ( from zerver.models import (
Attachment, Attachment,
CustomProfileField, CustomProfileField,
@ -231,6 +232,7 @@ from zerver.models import (
RealmUserDefault, RealmUserDefault,
Service, Service,
Stream, Stream,
UserGroup,
UserMessage, UserMessage,
UserPresence, UserPresence,
UserProfile, UserProfile,
@ -1696,6 +1698,34 @@ class NormalActionsTest(BaseAction):
self.user_profile.realm, "backend", [othello], "Backend team", acting_user=None self.user_profile.realm, "backend", [othello], "Backend team", acting_user=None
) )
check_user_group_add("events[0]", events[0]) check_user_group_add("events[0]", events[0])
everyone_group = NamedUserGroup.objects.get(
name=SystemGroups.EVERYONE, realm=self.user_profile.realm, is_system_group=True
)
self.assertEqual(events[0]["group"]["can_mention_group"], everyone_group.id)
moderators_group = NamedUserGroup.objects.get(
name=SystemGroups.MODERATORS, realm=self.user_profile.realm, is_system_group=True
)
user_group = UserGroup.objects.create(realm=self.user_profile.realm)
user_group.direct_members.set([othello])
user_group.direct_subgroups.set([moderators_group])
with self.verify_action() as events:
check_add_user_group(
self.user_profile.realm,
"frontend",
[othello],
"",
{"can_mention_group": user_group},
acting_user=None,
)
check_user_group_add("events[0]", events[0])
self.assertEqual(
events[0]["group"]["can_mention_group"],
AnonymousSettingGroupDict(
direct_members=[othello.id], direct_subgroups=[moderators_group.id]
),
)
# Test name update # Test name update
backend = NamedUserGroup.objects.get(name="backend") backend = NamedUserGroup.objects.get(name="backend")
@ -1710,9 +1740,6 @@ class NormalActionsTest(BaseAction):
check_user_group_update("events[0]", events[0], "description") check_user_group_update("events[0]", events[0], "description")
# Test can_mention_group setting update # Test can_mention_group setting update
moderators_group = NamedUserGroup.objects.get(
name="role:moderators", realm=self.user_profile.realm, is_system_group=True
)
with self.verify_action() as events: with self.verify_action() as events:
do_change_user_group_permission_setting( do_change_user_group_permission_setting(
backend, "can_mention_group", moderators_group, acting_user=None backend, "can_mention_group", moderators_group, acting_user=None

View File

@ -434,6 +434,47 @@ class UserGroupAPITestCase(UserGroupTestCase):
marketing_group = NamedUserGroup.objects.get(name="marketing", realm=hamlet.realm) marketing_group = NamedUserGroup.objects.get(name="marketing", realm=hamlet.realm)
self.assertEqual(marketing_group.can_mention_group, nobody_group.usergroup_ptr) self.assertEqual(marketing_group.can_mention_group, nobody_group.usergroup_ptr)
othello = self.example_user("othello")
params = {
"name": "backend",
"members": orjson.dumps([hamlet.id]).decode(),
"description": "Backend team",
"can_mention_group": orjson.dumps(
{
"direct_members": [othello.id],
"direct_subgroups": [leadership_group.id, moderators_group.id],
}
).decode(),
}
result = self.client_post("/json/user_groups/create", info=params)
self.assert_json_success(result)
backend_group = NamedUserGroup.objects.get(name="backend", realm=hamlet.realm)
self.assertCountEqual(
list(backend_group.can_mention_group.direct_members.all()),
[othello],
)
self.assertCountEqual(
list(backend_group.can_mention_group.direct_subgroups.all()),
[leadership_group, moderators_group],
)
params = {
"name": "help",
"members": orjson.dumps([hamlet.id]).decode(),
"description": "Troubleshooting team",
"can_mention_group": orjson.dumps(
{
"direct_members": [],
"direct_subgroups": [moderators_group.id],
}
).decode(),
}
result = self.client_post("/json/user_groups/create", info=params)
self.assert_json_success(result)
help_group = NamedUserGroup.objects.get(name="help", realm=hamlet.realm)
# We do not create a new UserGroup object in such case.
self.assertEqual(help_group.can_mention_group_id, moderators_group.id)
internet_group = NamedUserGroup.objects.get( internet_group = NamedUserGroup.objects.get(
name="role:internet", realm=hamlet.realm, is_system_group=True name="role:internet", realm=hamlet.realm, is_system_group=True
) )
@ -471,6 +512,34 @@ class UserGroupAPITestCase(UserGroupTestCase):
result = self.client_post("/json/user_groups/create", info=params) result = self.client_post("/json/user_groups/create", info=params)
self.assert_json_error(result, "Invalid user group") self.assert_json_error(result, "Invalid user group")
params = {
"name": "frontend",
"members": orjson.dumps([hamlet.id]).decode(),
"description": "Frontend team",
"can_mention_group": orjson.dumps(
{
"direct_members": [1111],
"direct_subgroups": [leadership_group.id, moderators_group.id],
}
).decode(),
}
result = self.client_post("/json/user_groups/create", info=params)
self.assert_json_error(result, "Invalid user ID: 1111")
params = {
"name": "frontend",
"members": orjson.dumps([hamlet.id]).decode(),
"description": "Frontend team",
"can_mention_group": orjson.dumps(
{
"direct_members": [othello.id],
"direct_subgroups": [1111, moderators_group.id],
}
).decode(),
}
result = self.client_post("/json/user_groups/create", info=params)
self.assert_json_error(result, "Invalid user group ID: 1111")
def test_user_group_get(self) -> None: def test_user_group_get(self) -> None:
# Test success # Test success
user_profile = self.example_user("hamlet") user_profile = self.example_user("hamlet")

View File

@ -1,4 +1,4 @@
from typing import List, Optional, Sequence from typing import Dict, List, Optional, Sequence, Union
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
@ -23,7 +23,9 @@ from zerver.lib.exceptions import JsonableError
from zerver.lib.mention import MentionBackend, silent_mention_syntax_for_user from zerver.lib.mention import MentionBackend, silent_mention_syntax_for_user
from zerver.lib.request import REQ, has_request_variables from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.types import Validator
from zerver.lib.user_groups import ( from zerver.lib.user_groups import (
AnonymousSettingGroupDict,
access_user_group_by_id, access_user_group_by_id,
access_user_group_for_setting, access_user_group_for_setting,
check_user_group_name, check_user_group_name,
@ -36,12 +38,40 @@ from zerver.lib.user_groups import (
user_groups_in_realm_serialized, user_groups_in_realm_serialized,
) )
from zerver.lib.users import access_user_by_id, user_ids_to_users from zerver.lib.users import access_user_by_id, user_ids_to_users
from zerver.lib.validator import check_bool, check_int, check_list from zerver.lib.validator import check_bool, check_dict_only, check_int, check_list, check_union
from zerver.models import NamedUserGroup, UserProfile from zerver.models import NamedUserGroup, UserProfile
from zerver.models.users import get_system_bot from zerver.models.users import get_system_bot
from zerver.views.streams import compose_views from zerver.views.streams import compose_views
def parse_group_setting_value(
setting_value: Union[int, Dict[str, List[int]]],
) -> Union[int, AnonymousSettingGroupDict]:
if isinstance(setting_value, int):
return setting_value
if len(setting_value["direct_members"]) == 0 and len(setting_value["direct_subgroups"]) == 1:
return setting_value["direct_subgroups"][0]
return AnonymousSettingGroupDict(
direct_members=setting_value["direct_members"],
direct_subgroups=setting_value["direct_subgroups"],
)
check_group_setting: Validator[Union[int, Dict[str, List[int]]]] = check_union(
[
check_int,
check_dict_only(
[
("direct_members", check_list(check_int)),
("direct_subgroups", check_list(check_int)),
]
),
]
)
@require_user_group_edit_permission @require_user_group_edit_permission
@has_request_variables @has_request_variables
def add_user_group( def add_user_group(
@ -50,8 +80,8 @@ def add_user_group(
name: str = REQ(), name: str = REQ(),
members: Sequence[int] = REQ(json_validator=check_list(check_int), default=[]), members: Sequence[int] = REQ(json_validator=check_list(check_int), default=[]),
description: str = REQ(), description: str = REQ(),
can_mention_group_id: Optional[int] = REQ( can_mention_group: Optional[Union[Dict[str, List[int]], int]] = REQ(
"can_mention_group", json_validator=check_int, default=None json_validator=check_group_setting, default=None
), ),
) -> HttpResponse: ) -> HttpResponse:
user_profiles = user_ids_to_users(members, user_profile.realm) user_profiles = user_ids_to_users(members, user_profile.realm)
@ -60,15 +90,13 @@ def add_user_group(
group_settings_map = {} group_settings_map = {}
request_settings_dict = locals() request_settings_dict = locals()
for setting_name, permission_config in NamedUserGroup.GROUP_PERMISSION_SETTINGS.items(): for setting_name, permission_config in NamedUserGroup.GROUP_PERMISSION_SETTINGS.items():
setting_group_id_name = permission_config.id_field_name if setting_name not in request_settings_dict: # nocoverage
if setting_group_id_name not in request_settings_dict: # nocoverage
continue continue
if request_settings_dict[setting_group_id_name] is not None: if request_settings_dict[setting_name] is not None:
setting_value_group_id = request_settings_dict[setting_group_id_name] setting_value = parse_group_setting_value(request_settings_dict[setting_name])
setting_value_group = access_user_group_for_setting( setting_value_group = access_user_group_for_setting(
setting_value_group_id, setting_value,
user_profile, user_profile,
setting_name=setting_name, setting_name=setting_name,
permission_configuration=permission_config, permission_configuration=permission_config,