mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 12:03:46 +00:00 
			
		
		
		
	streams: Backend changes to support anonymous groups.
can_remove_subscribers_group setting can now be set to anonymous user groups. Co-authored-by: Sahil Batra <sahil@zulip.com>
This commit is contained in:
		
				
					committed by
					
						 Tim Abbott
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							005a27a9cf
						
					
				
				
					commit
					b6ebf143cc
				
			| @@ -20,6 +20,18 @@ format used by the Zulip server that they are interacting with. | ||||
|  | ||||
| ## Changes in Zulip 10.0 | ||||
|  | ||||
| **Feature level 320** | ||||
|  | ||||
| * [`GET /users/me/subscriptions`](/api/get-subscriptions), | ||||
|   [`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events), | ||||
|   [`POST /register`](/api/register-queue): `can_remove_subscribers_group` | ||||
|   field can now 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. | ||||
| * [`POST /users/me/subscriptions`](/api/subscribe), | ||||
|   [`PATCH /streams/{stream_id}`](/api/update-stream): The | ||||
|   `can_remove_subscribers_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 319** | ||||
|  | ||||
| * [Markdown message | ||||
|   | ||||
| @@ -34,7 +34,9 @@ DESKTOP_WARNING_VERSION = "5.9.3" | ||||
| # new level means in api_docs/changelog.md, as well as "**Changes**" | ||||
| # entries in the endpoint's documentation in `zulip.yaml`. | ||||
|  | ||||
| API_FEATURE_LEVEL = 319  # Last bumped for message-link class | ||||
| API_FEATURE_LEVEL = ( | ||||
|     320  # Last bumped for supporting anonymous groups for can_remove_subscribers_group | ||||
| ) | ||||
|  | ||||
| # 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 | ||||
|   | ||||
| @@ -40,13 +40,18 @@ from zerver.lib.streams import ( | ||||
|     can_access_stream_user_ids, | ||||
|     check_basic_stream_access, | ||||
|     get_occupied_streams, | ||||
|     get_setting_values_for_group_settings, | ||||
|     get_stream_permission_policy_name, | ||||
|     render_stream_description, | ||||
|     send_stream_creation_event, | ||||
|     stream_to_dict, | ||||
| ) | ||||
| from zerver.lib.subscription_info import get_subscribers_query | ||||
| from zerver.lib.types import APISubscriptionDict | ||||
| from zerver.lib.types import AnonymousSettingGroupDict, APISubscriptionDict | ||||
| from zerver.lib.user_groups import ( | ||||
|     get_group_setting_value_for_api, | ||||
|     get_group_setting_value_for_audit_log_data, | ||||
| ) | ||||
| from zerver.lib.users import ( | ||||
|     get_subscribers_of_target_user_subscriptions, | ||||
|     get_users_involved_in_dms_with_target_users, | ||||
| @@ -349,13 +354,21 @@ def send_subscription_add_events( | ||||
|                 subscribers = list(subscriber_dict[stream.id]) | ||||
|             stream_subscribers_dict[stream.id] = subscribers | ||||
|  | ||||
|     setting_group_ids = set() | ||||
|     for sub_info in sub_info_list: | ||||
|         stream = sub_info.stream | ||||
|         for setting_name in Stream.stream_permission_group_settings: | ||||
|             setting_group_ids.add(getattr(stream, setting_name + "_id")) | ||||
|  | ||||
|     setting_groups_dict = get_setting_values_for_group_settings(list(setting_group_ids)) | ||||
|  | ||||
|     for user_id, sub_infos in info_by_user.items(): | ||||
|         sub_dicts: list[APISubscriptionDict] = [] | ||||
|         for sub_info in sub_infos: | ||||
|             stream = sub_info.stream | ||||
|             stream_subscribers = stream_subscribers_dict[stream.id] | ||||
|             subscription = sub_info.sub | ||||
|             stream_dict = stream_to_dict(stream, recent_traffic) | ||||
|             stream_dict = stream_to_dict(stream, recent_traffic, setting_groups_dict) | ||||
|             # This is verbose as we cannot unpack existing TypedDict | ||||
|             # to initialize another TypedDict while making mypy happy. | ||||
|             # https://github.com/python/mypy/issues/5382 | ||||
| @@ -447,6 +460,14 @@ def send_stream_creation_events_for_previously_inaccessible_streams( | ||||
|     stream_ids = set(altered_user_dict.keys()) | ||||
|     recent_traffic = get_streams_traffic(stream_ids, realm) | ||||
|  | ||||
|     setting_group_ids = set() | ||||
|     for stream_id in stream_ids: | ||||
|         stream = stream_dict[stream_id] | ||||
|         for setting_name in Stream.stream_permission_group_settings: | ||||
|             setting_group_ids.add(getattr(stream, setting_name + "_id")) | ||||
|  | ||||
|     setting_groups_dict = get_setting_values_for_group_settings(list(setting_group_ids)) | ||||
|  | ||||
|     for stream_id, stream_users_ids in altered_user_dict.items(): | ||||
|         stream = stream_dict[stream_id] | ||||
|  | ||||
| @@ -467,7 +488,9 @@ def send_stream_creation_events_for_previously_inaccessible_streams( | ||||
|             notify_user_ids = list(stream_users_ids & altered_guests) | ||||
|  | ||||
|         if notify_user_ids: | ||||
|             send_stream_creation_event(realm, stream, notify_user_ids, recent_traffic) | ||||
|             send_stream_creation_event( | ||||
|                 realm, stream, notify_user_ids, recent_traffic, setting_groups_dict | ||||
|             ) | ||||
|  | ||||
|  | ||||
| def send_peer_subscriber_events( | ||||
| @@ -1561,16 +1584,30 @@ def do_change_stream_group_based_setting( | ||||
|     setting_name: str, | ||||
|     user_group: UserGroup, | ||||
|     *, | ||||
|     old_setting_api_value: int | AnonymousSettingGroupDict | None = None, | ||||
|     acting_user: UserProfile | None = None, | ||||
| ) -> None: | ||||
|     old_user_group = getattr(stream, setting_name) | ||||
|     old_user_group_id = None | ||||
|     if old_user_group is not None: | ||||
|         old_user_group_id = old_user_group.id | ||||
|  | ||||
|     setattr(stream, setting_name, user_group) | ||||
|     stream.save() | ||||
|  | ||||
|     if old_setting_api_value is None: | ||||
|         # Most production callers will have computed this as part of | ||||
|         # verifying whether there's an actual change to make, but it | ||||
|         # feels quite clumsy to have to pass it from unit tests, so we | ||||
|         # compute it here if not provided by the caller. | ||||
|         old_setting_api_value = get_group_setting_value_for_api(old_user_group) | ||||
|     new_setting_api_value = get_group_setting_value_for_api(user_group) | ||||
|  | ||||
|     if not hasattr(old_user_group, "named_user_group") and hasattr(user_group, "named_user_group"): | ||||
|         # We delete the UserGroup which the setting was set to | ||||
|         # previously if it does not have any linked NamedUserGroup | ||||
|         # object, as it is not used anywhere else. A new UserGroup | ||||
|         # object would be created if the setting is later set to | ||||
|         # a combination of users and groups. | ||||
|         old_user_group.delete() | ||||
|  | ||||
|     RealmAuditLog.objects.create( | ||||
|         realm=stream.realm, | ||||
|         acting_user=acting_user, | ||||
| @@ -1578,8 +1615,12 @@ def do_change_stream_group_based_setting( | ||||
|         event_type=AuditLogEventType.CHANNEL_GROUP_BASED_SETTING_CHANGED, | ||||
|         event_time=timezone_now(), | ||||
|         extra_data={ | ||||
|             RealmAuditLog.OLD_VALUE: old_user_group_id, | ||||
|             RealmAuditLog.NEW_VALUE: user_group.id, | ||||
|             RealmAuditLog.OLD_VALUE: get_group_setting_value_for_audit_log_data( | ||||
|                 old_setting_api_value | ||||
|             ), | ||||
|             RealmAuditLog.NEW_VALUE: get_group_setting_value_for_audit_log_data( | ||||
|                 new_setting_api_value | ||||
|             ), | ||||
|             "property": setting_name, | ||||
|         }, | ||||
|     ) | ||||
| @@ -1587,7 +1628,7 @@ def do_change_stream_group_based_setting( | ||||
|         op="update", | ||||
|         type="stream", | ||||
|         property=setting_name, | ||||
|         value=user_group.id, | ||||
|         value=new_setting_api_value, | ||||
|         stream_id=stream.id, | ||||
|         name=stream.name, | ||||
|     ) | ||||
|   | ||||
| @@ -45,13 +45,26 @@ from zerver.lib.data_types import ( | ||||
|     make_checker, | ||||
| ) | ||||
| from zerver.lib.topic import ORIG_TOPIC, TOPIC_LINKS, TOPIC_NAME | ||||
| from zerver.lib.types import AnonymousSettingGroupDict | ||||
| from zerver.models import Realm, RealmUserDefault, Stream, UserProfile | ||||
|  | ||||
| group_setting_type = UnionType( | ||||
|     [ | ||||
|         int, | ||||
|         DictType( | ||||
|             required_keys=[ | ||||
|                 ("direct_members", ListType(int)), | ||||
|                 ("direct_subgroups", ListType(int)), | ||||
|             ] | ||||
|         ), | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| # These fields are used for "stream" events, and are included in the | ||||
| # larger "subscription" events that also contain personal settings. | ||||
| default_stream_fields = [ | ||||
|     ("is_archived", bool), | ||||
|     ("can_remove_subscribers_group", int), | ||||
|     ("can_remove_subscribers_group", group_setting_type), | ||||
|     ("creator_id", OptionalType(int)), | ||||
|     ("date_created", int), | ||||
|     ("description", str), | ||||
| @@ -103,6 +116,12 @@ optional_value_type = UnionType( | ||||
|         bool, | ||||
|         int, | ||||
|         str, | ||||
|         DictType( | ||||
|             required_keys=[ | ||||
|                 ("direct_members", ListType(int)), | ||||
|                 ("direct_subgroups", ListType(int)), | ||||
|             ] | ||||
|         ), | ||||
|         Equals(None), | ||||
|     ] | ||||
| ) | ||||
| @@ -1046,18 +1065,6 @@ night_logo_data = DictType( | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| group_setting_type = UnionType( | ||||
|     [ | ||||
|         int, | ||||
|         DictType( | ||||
|             required_keys=[ | ||||
|                 ("direct_members", ListType(int)), | ||||
|                 ("direct_subgroups", ListType(int)), | ||||
|             ] | ||||
|         ), | ||||
|     ] | ||||
| ) | ||||
|  | ||||
| group_setting_update_data_type = DictType( | ||||
|     required_keys=[], | ||||
|     optional_keys=[ | ||||
| @@ -1442,7 +1449,7 @@ def check_stream_update( | ||||
|         assert value in Stream.STREAM_POST_POLICY_TYPES | ||||
|     elif prop == "can_remove_subscribers_group": | ||||
|         assert extra_keys == set() | ||||
|         assert isinstance(value, int) | ||||
|         assert isinstance(value, int | AnonymousSettingGroupDict) | ||||
|     elif prop == "first_message_id": | ||||
|         assert extra_keys == set() | ||||
|         assert isinstance(value, int) | ||||
|   | ||||
| @@ -21,10 +21,14 @@ from zerver.lib.stream_subscription import ( | ||||
| from zerver.lib.stream_traffic import get_average_weekly_stream_traffic, get_streams_traffic | ||||
| from zerver.lib.string_validation import check_stream_name | ||||
| from zerver.lib.timestamp import datetime_to_timestamp | ||||
| from zerver.lib.types import APIStreamDict | ||||
| from zerver.lib.user_groups import user_has_permission_for_group_setting | ||||
| from zerver.lib.types import AnonymousSettingGroupDict, APIStreamDict | ||||
| from zerver.lib.user_groups import ( | ||||
|     get_group_setting_value_for_api, | ||||
|     user_has_permission_for_group_setting, | ||||
| ) | ||||
| from zerver.models import ( | ||||
|     DefaultStreamGroup, | ||||
|     GroupGroupMembership, | ||||
|     NamedUserGroup, | ||||
|     Realm, | ||||
|     RealmAuditLog, | ||||
| @@ -32,6 +36,7 @@ from zerver.models import ( | ||||
|     Stream, | ||||
|     Subscription, | ||||
|     UserGroup, | ||||
|     UserGroupMembership, | ||||
|     UserProfile, | ||||
| ) | ||||
| from zerver.models.groups import SystemGroups | ||||
| @@ -123,8 +128,13 @@ def send_stream_creation_event( | ||||
|     stream: Stream, | ||||
|     user_ids: list[int], | ||||
|     recent_traffic: dict[int, int] | None = None, | ||||
|     setting_groups_dict: dict[int, int | AnonymousSettingGroupDict] | None = None, | ||||
| ) -> None: | ||||
|     event = dict(type="stream", op="create", streams=[stream_to_dict(stream, recent_traffic)]) | ||||
|     event = dict( | ||||
|         type="stream", | ||||
|         op="create", | ||||
|         streams=[stream_to_dict(stream, recent_traffic, setting_groups_dict)], | ||||
|     ) | ||||
|     send_event_on_commit(realm, event, user_ids) | ||||
|  | ||||
|  | ||||
| @@ -870,7 +880,11 @@ def get_occupied_streams(realm: Realm) -> QuerySet[Stream]: | ||||
|     return occupied_streams | ||||
|  | ||||
|  | ||||
| def stream_to_dict(stream: Stream, recent_traffic: dict[int, int] | None = None) -> APIStreamDict: | ||||
| def stream_to_dict( | ||||
|     stream: Stream, | ||||
|     recent_traffic: dict[int, int] | None = None, | ||||
|     setting_groups_dict: dict[int, int | AnonymousSettingGroupDict] | None = None, | ||||
| ) -> APIStreamDict: | ||||
|     if recent_traffic is not None: | ||||
|         stream_weekly_traffic = get_average_weekly_stream_traffic( | ||||
|             stream.id, stream.date_created, recent_traffic | ||||
| @@ -884,9 +898,16 @@ def stream_to_dict(stream: Stream, recent_traffic: dict[int, int] | None = None) | ||||
|         # passing stream data to spectators. | ||||
|         stream_weekly_traffic = None | ||||
|  | ||||
|     if setting_groups_dict is not None: | ||||
|         can_remove_subscribers_group = setting_groups_dict[stream.can_remove_subscribers_group_id] | ||||
|     else: | ||||
|         can_remove_subscribers_group = get_group_setting_value_for_api( | ||||
|             stream.can_remove_subscribers_group | ||||
|         ) | ||||
|  | ||||
|     return APIStreamDict( | ||||
|         is_archived=stream.deactivated, | ||||
|         can_remove_subscribers_group=stream.can_remove_subscribers_group_id, | ||||
|         can_remove_subscribers_group=can_remove_subscribers_group, | ||||
|         creator_id=stream.creator_id, | ||||
|         date_created=datetime_to_timestamp(stream.date_created), | ||||
|         description=stream.description, | ||||
| @@ -978,6 +999,38 @@ def get_streams_for_user( | ||||
|     return list(streams) | ||||
|  | ||||
|  | ||||
| def get_setting_values_for_group_settings( | ||||
|     group_ids: list[int], | ||||
| ) -> dict[int, int | AnonymousSettingGroupDict]: | ||||
|     setting_value_dict = {} | ||||
|     setting_groups = UserGroup.objects.filter(id__in=group_ids).select_related("named_user_group") | ||||
|     anonymous_groups = [] | ||||
|     for group in setting_groups: | ||||
|         if hasattr(group, "named_user_group"): | ||||
|             setting_value_dict[group.id] = group.id | ||||
|         else: | ||||
|             setting_value_dict[group.id] = AnonymousSettingGroupDict( | ||||
|                 direct_members=[], | ||||
|                 direct_subgroups=[], | ||||
|             ) | ||||
|             anonymous_groups.append(group.id) | ||||
|  | ||||
|     group_members = ( | ||||
|         UserGroupMembership.objects.filter(user_group_id__in=anonymous_groups) | ||||
|         .exclude(user_profile__is_active=False) | ||||
|         .values_list("user_group_id", "user_profile_id") | ||||
|     ) | ||||
|     group_subgroups = GroupGroupMembership.objects.filter( | ||||
|         supergroup_id__in=anonymous_groups | ||||
|     ).values_list("supergroup_id", "subgroup_id") | ||||
|     for user_group_id, user_profile_id in group_members: | ||||
|         setting_value_dict[user_group_id].direct_members.append(user_profile_id) | ||||
|     for supergroup_id, subgroup_id in group_subgroups: | ||||
|         setting_value_dict[supergroup_id].direct_subgroups.append(subgroup_id) | ||||
|  | ||||
|     return setting_value_dict | ||||
|  | ||||
|  | ||||
| def do_get_streams( | ||||
|     user_profile: UserProfile, | ||||
|     include_public: bool = True, | ||||
| @@ -1003,14 +1056,22 @@ def do_get_streams( | ||||
|     stream_ids = {stream.id for stream in streams} | ||||
|     recent_traffic = get_streams_traffic(stream_ids, user_profile.realm) | ||||
|  | ||||
|     setting_group_ids = set() | ||||
|     for stream in streams: | ||||
|         for setting_name in Stream.stream_permission_group_settings: | ||||
|             setting_group_ids.add(getattr(stream, setting_name + "_id")) | ||||
|  | ||||
|     setting_groups_dict = get_setting_values_for_group_settings(list(setting_group_ids)) | ||||
|  | ||||
|     stream_dicts = sorted( | ||||
|         (stream_to_dict(stream, recent_traffic) for stream in streams), key=lambda elt: elt["name"] | ||||
|         (stream_to_dict(stream, recent_traffic, setting_groups_dict) for stream in streams), | ||||
|         key=lambda elt: elt["name"], | ||||
|     ) | ||||
|  | ||||
|     if include_default: | ||||
|         default_stream_ids = get_default_stream_ids_for_realm(user_profile.realm_id) | ||||
|         for stream in stream_dicts: | ||||
|             stream["is_default"] = stream["stream_id"] in default_stream_ids | ||||
|         for stream_dict in stream_dicts: | ||||
|             stream_dict["is_default"] = stream_dict["stream_id"] in default_stream_ids | ||||
|  | ||||
|     return stream_dicts | ||||
|  | ||||
|   | ||||
| @@ -16,9 +16,14 @@ from zerver.lib.stream_subscription import ( | ||||
|     get_stream_subscriptions_for_user, | ||||
| ) | ||||
| from zerver.lib.stream_traffic import get_average_weekly_stream_traffic, get_streams_traffic | ||||
| from zerver.lib.streams import get_web_public_streams_queryset, subscribed_to_stream | ||||
| from zerver.lib.streams import ( | ||||
|     get_setting_values_for_group_settings, | ||||
|     get_web_public_streams_queryset, | ||||
|     subscribed_to_stream, | ||||
| ) | ||||
| from zerver.lib.timestamp import datetime_to_timestamp | ||||
| from zerver.lib.types import ( | ||||
|     AnonymousSettingGroupDict, | ||||
|     APIStreamDict, | ||||
|     NeverSubscribedStreamDict, | ||||
|     RawStreamDict, | ||||
| @@ -26,6 +31,7 @@ from zerver.lib.types import ( | ||||
|     SubscriptionInfo, | ||||
|     SubscriptionStreamDict, | ||||
| ) | ||||
| from zerver.lib.user_groups import get_group_setting_value_for_api | ||||
| from zerver.models import Realm, Stream, Subscription, UserProfile | ||||
| from zerver.models.streams import get_all_streams | ||||
|  | ||||
| @@ -43,7 +49,9 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo: | ||||
|     for stream in get_web_public_streams_queryset(realm): | ||||
|         # Add Stream fields. | ||||
|         is_archived = stream.deactivated | ||||
|         can_remove_subscribers_group_id = stream.can_remove_subscribers_group_id | ||||
|         can_remove_subscribers_group = get_group_setting_value_for_api( | ||||
|             stream.can_remove_subscribers_group | ||||
|         ) | ||||
|         creator_id = stream.creator_id | ||||
|         date_created = datetime_to_timestamp(stream.date_created) | ||||
|         description = stream.description | ||||
| @@ -76,7 +84,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo: | ||||
|         sub = SubscriptionStreamDict( | ||||
|             is_archived=is_archived, | ||||
|             audible_notifications=audible_notifications, | ||||
|             can_remove_subscribers_group=can_remove_subscribers_group_id, | ||||
|             can_remove_subscribers_group=can_remove_subscribers_group, | ||||
|             color=color, | ||||
|             creator_id=creator_id, | ||||
|             date_created=date_created, | ||||
| @@ -118,7 +126,9 @@ def build_unsubscribed_sub_from_stream_dict( | ||||
|  | ||||
|  | ||||
| def build_stream_api_dict( | ||||
|     raw_stream_dict: RawStreamDict, recent_traffic: dict[int, int] | None | ||||
|     raw_stream_dict: RawStreamDict, | ||||
|     recent_traffic: dict[int, int] | None, | ||||
|     setting_groups_dict: dict[int, int | AnonymousSettingGroupDict], | ||||
| ) -> APIStreamDict: | ||||
|     # Add a few computed fields not directly from the data models. | ||||
|     if recent_traffic is not None: | ||||
| @@ -133,9 +143,13 @@ def build_stream_api_dict( | ||||
|     # migration. | ||||
|     is_announcement_only = raw_stream_dict["stream_post_policy"] == Stream.STREAM_POST_POLICY_ADMINS | ||||
|  | ||||
|     can_remove_subscribers_group = setting_groups_dict[ | ||||
|         raw_stream_dict["can_remove_subscribers_group_id"] | ||||
|     ] | ||||
|  | ||||
|     return APIStreamDict( | ||||
|         is_archived=raw_stream_dict["deactivated"], | ||||
|         can_remove_subscribers_group=raw_stream_dict["can_remove_subscribers_group_id"], | ||||
|         can_remove_subscribers_group=can_remove_subscribers_group, | ||||
|         creator_id=raw_stream_dict["creator_id"], | ||||
|         date_created=datetime_to_timestamp(raw_stream_dict["date_created"]), | ||||
|         description=raw_stream_dict["description"], | ||||
| @@ -471,6 +485,13 @@ def gather_subscriptions_helper( | ||||
|     } | ||||
|     all_streams_map: dict[int, RawStreamDict] = {stream["id"]: stream for stream in all_streams} | ||||
|  | ||||
|     setting_group_ids = set() | ||||
|     for stream in all_streams: | ||||
|         for setting_name in Stream.stream_permission_group_settings: | ||||
|             setting_group_ids.add(stream[setting_name + "_id"]) | ||||
|  | ||||
|     setting_groups_dict = get_setting_values_for_group_settings(list(setting_group_ids)) | ||||
|  | ||||
|     sub_dicts_query: Iterable[RawSubscriptionDict] = ( | ||||
|         get_stream_subscriptions_for_user(user_profile) | ||||
|         .values( | ||||
| @@ -505,7 +526,9 @@ def gather_subscriptions_helper( | ||||
|         stream_id = get_stream_id(sub_dict) | ||||
|         sub_unsub_stream_ids.add(stream_id) | ||||
|         raw_stream_dict = all_streams_map[stream_id] | ||||
|         stream_api_dict = build_stream_api_dict(raw_stream_dict, recent_traffic) | ||||
|         stream_api_dict = build_stream_api_dict( | ||||
|             raw_stream_dict, recent_traffic, setting_groups_dict | ||||
|         ) | ||||
|         stream_dict = build_stream_dict_for_sub( | ||||
|             user=user_profile, | ||||
|             sub_dict=sub_dict, | ||||
|   | ||||
| @@ -191,7 +191,7 @@ class SubscriptionStreamDict(TypedDict): | ||||
|     """ | ||||
|  | ||||
|     audible_notifications: bool | None | ||||
|     can_remove_subscribers_group: int | ||||
|     can_remove_subscribers_group: int | AnonymousSettingGroupDict | ||||
|     color: str | ||||
|     creator_id: int | None | ||||
|     date_created: int | ||||
| @@ -245,7 +245,7 @@ class DefaultStreamDict(TypedDict): | ||||
|     """ | ||||
|  | ||||
|     is_archived: bool | ||||
|     can_remove_subscribers_group: int | ||||
|     can_remove_subscribers_group: int | AnonymousSettingGroupDict | ||||
|     creator_id: int | None | ||||
|     date_created: int | ||||
|     description: str | ||||
|   | ||||
| @@ -143,7 +143,7 @@ class Stream(models.Model): | ||||
|  | ||||
|     stream_permission_group_settings = { | ||||
|         "can_remove_subscribers_group": GroupPermissionSetting( | ||||
|             require_system_group=True, | ||||
|             require_system_group=False, | ||||
|             allow_internet_group=False, | ||||
|             allow_owners_group=False, | ||||
|             allow_nobody_group=False, | ||||
|   | ||||
| @@ -1459,7 +1459,33 @@ paths: | ||||
|                                 value: | ||||
|                                   description: | | ||||
|                                     The new value of the changed property. | ||||
| 
 | ||||
|                                     **Changes**: Starting with Zulip 10.0 (feature level 320), this | ||||
|                                     field can be an object for `can_remove_subscribers_group` property, | ||||
|                                     which is a [group-setting value][setting-values], when the setting | ||||
|                                     is set to a combination of users and groups. | ||||
| 
 | ||||
|                                     [setting-values]: /api/group-setting-values | ||||
|                                   oneOf: | ||||
|                                     - type: object | ||||
|                                       additionalProperties: false | ||||
|                                       properties: | ||||
|                                         direct_members: | ||||
|                                           description: | | ||||
|                                             The list of IDs of individual users in the collection of users with this permission. | ||||
|                                           type: array | ||||
|                                           items: | ||||
|                                             type: integer | ||||
|                                         direct_subgroups: | ||||
|                                           description: | | ||||
|                                             The list of IDs of the groups in the collection of users with this permission. | ||||
|                                           type: array | ||||
|                                           items: | ||||
|                                             type: integer | ||||
|                                       description: | | ||||
|                                         If an object, it will be a [group-setting value][setting-values] with these fields: | ||||
| 
 | ||||
|                                         [setting-values]: /api/group-setting-values | ||||
|                                     - type: integer | ||||
|                                     - type: boolean | ||||
|                                     - type: string | ||||
| @@ -10364,7 +10390,7 @@ paths: | ||||
|                 message_retention_days: | ||||
|                   $ref: "#/components/schemas/MessageRetentionDays" | ||||
|                 can_remove_subscribers_group: | ||||
|                   $ref: "#/components/schemas/CanRemoveSubscribersGroupId" | ||||
|                   $ref: "#/components/schemas/CanRemoveSubscribersGroup" | ||||
|               required: | ||||
|                 - subscriptions | ||||
|             encoding: | ||||
| @@ -19958,7 +19984,43 @@ paths: | ||||
|                 message_retention_days: | ||||
|                   $ref: "#/components/schemas/MessageRetentionDays" | ||||
|                 can_remove_subscribers_group: | ||||
|                   $ref: "#/components/schemas/CanRemoveSubscribersGroupId" | ||||
|                   description: | | ||||
|                     The set of users who have permission to unsubscribe others from this | ||||
|                     channel expressed as an [update to a group-setting value][update-group-setting]. | ||||
| 
 | ||||
|                     Note that a user who is a member of the specified user group must | ||||
|                     also [have access](/help/channel-permissions) to the channel in | ||||
|                     order to unsubscribe others. | ||||
| 
 | ||||
|                     **Changes**: Prior to Zulip 10.0 (feature level 320), this value was | ||||
|                     always the integer ID of a system group. | ||||
| 
 | ||||
|                     Before Zulip 8.0 (feature level 197), the `can_remove_subscribers_group` | ||||
|                     setting was named `can_remove_subscribers_group_id`. | ||||
| 
 | ||||
|                     New in Zulip 7.0 (feature level 161). | ||||
|                   type: object | ||||
|                   additionalProperties: false | ||||
|                   properties: | ||||
|                     new: | ||||
|                       allOf: | ||||
|                         - description: | | ||||
|                             The new [group-setting value](/api/group-setting-values) for who would | ||||
|                             have the permission to remove subscribers from the channel. | ||||
|                         - $ref: "#/components/schemas/GroupSettingValue" | ||||
|                     old: | ||||
|                       allOf: | ||||
|                         - description: | | ||||
|                             The expected current [group-setting value](/api/group-setting-values) | ||||
|                             for who has the permission to remove subscribers from the channel. | ||||
|                         - $ref: "#/components/schemas/GroupSettingValue" | ||||
|                   required: | ||||
|                     - new | ||||
|                   example: | ||||
|                     { | ||||
|                       "new": {"direct_members": [10], "direct_subgroups": [11]}, | ||||
|                       "old": 15, | ||||
|                     } | ||||
|             encoding: | ||||
|               is_private: | ||||
|                 contentType: application/json | ||||
| @@ -21819,16 +21881,7 @@ components: | ||||
|             **Changes**: Deprecated in Zulip 3.0 (feature level 1). Clients | ||||
|             should use `stream_post_policy` instead. | ||||
|         can_remove_subscribers_group: | ||||
|           type: integer | ||||
|           description: | | ||||
|             ID of the user group whose members are allowed to unsubscribe others | ||||
|             from the channel. | ||||
| 
 | ||||
|             **Changes**: Before Zulip 8.0 (feature level 197), | ||||
|             the `can_remove_subscribers_group` setting | ||||
|             was named `can_remove_subscribers_group_id`. | ||||
| 
 | ||||
|             New in Zulip 6.0 (feature level 142). | ||||
|           $ref: "#/components/schemas/CanRemoveSubscribersGroup" | ||||
|     BasicBot: | ||||
|       allOf: | ||||
|         - $ref: "#/components/schemas/BasicBotBase" | ||||
| @@ -22835,16 +22888,7 @@ components: | ||||
|             If `null`, the channel was recently created and there is | ||||
|             insufficient data to estimate the average traffic. | ||||
|         can_remove_subscribers_group: | ||||
|           type: integer | ||||
|           description: | | ||||
|             ID of the user group whose members are allowed to unsubscribe others | ||||
|             from the channel. | ||||
| 
 | ||||
|             **Changes**: Before Zulip 8.0 (feature level 197), | ||||
|             the `can_remove_subscribers_group` setting | ||||
|             was named `can_remove_subscribers_group_id`. | ||||
| 
 | ||||
|             New in Zulip 6.0 (feature level 142). | ||||
|           $ref: "#/components/schemas/CanRemoveSubscribersGroup" | ||||
|         is_archived: | ||||
|           type: boolean | ||||
|           description: | | ||||
| @@ -24515,25 +24559,26 @@ components: | ||||
|         - type: string | ||||
|         - type: integer | ||||
|       example: "20" | ||||
|     CanRemoveSubscribersGroupId: | ||||
|       description: | | ||||
|         ID of the [user group](/api/get-user-groups) whose members are | ||||
|         allowed to unsubscribe others from the channel. Note that a user | ||||
|         who is a member of the specified user group must also [have | ||||
|         access](/help/channel-permissions) to the channel in order to | ||||
|         unsubscribe others. | ||||
|     CanRemoveSubscribersGroup: | ||||
|       allOf: | ||||
|         - $ref: "#/components/schemas/GroupSettingValue" | ||||
|         - description: | | ||||
|             A [group-setting value][setting-values] defining the set of users | ||||
|             who have permission to remove subscribers from this channel. | ||||
| 
 | ||||
|         This setting can currently only be set to user groups that are | ||||
|         system groups, except for the system groups named | ||||
|         `"role:internet"` and `"role:owners"`. | ||||
|             Note that a user who is a member of the specified user group must | ||||
|             also [have access](/help/channel-permissions) to the channel in | ||||
|             order to unsubscribe others. | ||||
| 
 | ||||
|         **Changes**: Before Zulip 8.0 (feature level 197), | ||||
|         the `can_remove_subscribers_group` setting | ||||
|         was named `can_remove_subscribers_group_id`. | ||||
|             **Changes**: Prior to Zulip 10.0 (feature level 320), this value was | ||||
|             always the integer ID of a system group. | ||||
| 
 | ||||
|         New in Zulip 7.0 (feature level 161). | ||||
|       type: integer | ||||
|       example: 20 | ||||
|             Before Zulip 8.0 (feature level 197), the `can_remove_subscribers_group` | ||||
|             setting was named `can_remove_subscribers_group_id`. | ||||
| 
 | ||||
|             New in Zulip 6.0 (feature level 142). | ||||
| 
 | ||||
|             [setting-values]: /api/group-setting-values | ||||
|     LinkifierPattern: | ||||
|       description: | | ||||
|         The [Python regular | ||||
|   | ||||
| @@ -1192,7 +1192,7 @@ class FetchQueriesTest(ZulipTestCase): | ||||
|         realm = get_realm_with_settings(realm_id=user.realm_id) | ||||
|  | ||||
|         with ( | ||||
|             self.assert_database_query_count(40), | ||||
|             self.assert_database_query_count(42), | ||||
|             mock.patch("zerver.lib.events.always_want") as want_mock, | ||||
|         ): | ||||
|             fetch_initial_state_data(user, realm=realm) | ||||
| @@ -1224,9 +1224,9 @@ class FetchQueriesTest(ZulipTestCase): | ||||
|             saved_snippets=1, | ||||
|             scheduled_messages=1, | ||||
|             starred_messages=1, | ||||
|             stream=3, | ||||
|             stream=4, | ||||
|             stop_words=0, | ||||
|             subscription=4, | ||||
|             subscription=5, | ||||
|             update_display_settings=0, | ||||
|             update_global_notifications=0, | ||||
|             update_message_flags=5, | ||||
|   | ||||
| @@ -4538,6 +4538,26 @@ class SubscribeActionTest(BaseAction): | ||||
|                 acting_user=self.example_user("hamlet"), | ||||
|             ) | ||||
|         check_stream_update("events[0]", events[0]) | ||||
|         self.assertEqual(events[0]["value"], moderators_group.id) | ||||
|  | ||||
|         setting_group = self.create_or_update_anonymous_group_for_setting( | ||||
|             [self.user_profile], | ||||
|             [moderators_group], | ||||
|         ) | ||||
|         with self.verify_action(include_subscribers=include_subscribers, num_events=1) as events: | ||||
|             do_change_stream_group_based_setting( | ||||
|                 stream, | ||||
|                 "can_remove_subscribers_group", | ||||
|                 setting_group, | ||||
|                 acting_user=self.example_user("hamlet"), | ||||
|             ) | ||||
|         check_stream_update("events[0]", events[0]) | ||||
|         self.assertEqual( | ||||
|             events[0]["value"], | ||||
|             AnonymousSettingGroupDict( | ||||
|                 direct_members=[self.user_profile.id], direct_subgroups=[moderators_group.id] | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|         # Subscribe to a totally new invite-only stream, so it's just Hamlet on it | ||||
|         stream = self.make_stream("private", self.user_profile.realm, invite_only=True) | ||||
|   | ||||
| @@ -271,7 +271,7 @@ class HomeTest(ZulipTestCase): | ||||
|  | ||||
|         # Verify succeeds once logged-in | ||||
|         with ( | ||||
|             self.assert_database_query_count(51), | ||||
|             self.assert_database_query_count(52), | ||||
|             patch("zerver.lib.cache.cache_set") as cache_mock, | ||||
|         ): | ||||
|             result = self._get_home_page(stream="Denmark") | ||||
| @@ -576,7 +576,7 @@ class HomeTest(ZulipTestCase): | ||||
|         # Verify number of queries for Realm admin isn't much higher than for normal users. | ||||
|         self.login("iago") | ||||
|         with ( | ||||
|             self.assert_database_query_count(51), | ||||
|             self.assert_database_query_count(52), | ||||
|             patch("zerver.lib.cache.cache_set") as cache_mock, | ||||
|         ): | ||||
|             result = self._get_home_page() | ||||
| @@ -608,7 +608,7 @@ class HomeTest(ZulipTestCase): | ||||
|         self._get_home_page() | ||||
|  | ||||
|         # Then for the second page load, measure the number of queries. | ||||
|         with self.assert_database_query_count(46): | ||||
|         with self.assert_database_query_count(47): | ||||
|             result = self._get_home_page() | ||||
|  | ||||
|         # Do a sanity check that our new streams were in the payload. | ||||
|   | ||||
| @@ -570,10 +570,31 @@ class TestCreateStreams(ZulipTestCase): | ||||
|             allow_fail=True, | ||||
|             subdomain="zulip", | ||||
|         ) | ||||
|         self.assert_json_error( | ||||
|             result, "'can_remove_subscribers_group' must be a system user group." | ||||
|         self.assert_json_success(result) | ||||
|         stream = get_stream("new_stream3", realm) | ||||
|         self.assertEqual(stream.can_remove_subscribers_group.id, hamletcharacters_group.id) | ||||
|  | ||||
|         subscriptions = [{"name": "new_stream4", "description": "Fourth new stream"}] | ||||
|         result = self.common_subscribe_to_streams( | ||||
|             user, | ||||
|             subscriptions, | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     {"direct_members": [user.id], "direct_subgroups": [moderators_system_group.id]} | ||||
|                 ).decode() | ||||
|             }, | ||||
|             allow_fail=True, | ||||
|             subdomain="zulip", | ||||
|         ) | ||||
|         self.assert_json_success(result) | ||||
|         stream = get_stream("new_stream4", realm) | ||||
|         self.assertEqual(list(stream.can_remove_subscribers_group.direct_members.all()), [user]) | ||||
|         self.assertEqual( | ||||
|             list(stream.can_remove_subscribers_group.direct_subgroups.all()), | ||||
|             [moderators_system_group], | ||||
|         ) | ||||
|  | ||||
|         subscriptions = [{"name": "new_stream5", "description": "Fifth new stream"}] | ||||
|         internet_group = NamedUserGroup.objects.get( | ||||
|             name="role:internet", is_system_group=True, realm=realm | ||||
|         ) | ||||
| @@ -2273,14 +2294,22 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         self.login("shiva") | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(moderators_system_group.id).decode()}, | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     {"new": moderators_system_group.id} | ||||
|                 ).decode() | ||||
|             }, | ||||
|         ) | ||||
|         self.assert_json_error(result, "Must be an organization administrator") | ||||
|  | ||||
|         self.login("iago") | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(moderators_system_group.id).decode()}, | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     {"new": moderators_system_group.id} | ||||
|                 ).decode() | ||||
|             }, | ||||
|         ) | ||||
|         self.assert_json_success(result) | ||||
|         stream = get_stream("stream_name1", realm) | ||||
| @@ -2290,10 +2319,56 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm) | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(hamletcharacters_group.id).decode()}, | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     {"new": hamletcharacters_group.id} | ||||
|                 ).decode() | ||||
|             }, | ||||
|         ) | ||||
|         self.assert_json_error( | ||||
|             result, "'can_remove_subscribers_group' must be a system user group." | ||||
|         self.assert_json_success(result) | ||||
|         stream = get_stream("stream_name1", realm) | ||||
|         self.assertEqual(stream.can_remove_subscribers_group.id, hamletcharacters_group.id) | ||||
|  | ||||
|         # Test changing it to anonymous group. | ||||
|         hamlet = self.example_user("hamlet") | ||||
|  | ||||
|         # Test passing incorrect old value. | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     { | ||||
|                         "new": { | ||||
|                             "direct_members": [hamlet.id], | ||||
|                             "direct_subgroups": [moderators_system_group.id], | ||||
|                         }, | ||||
|                         "old": moderators_system_group.id, | ||||
|                     } | ||||
|                 ).decode() | ||||
|             }, | ||||
|         ) | ||||
|         self.assert_json_error(result, "'old' value does not match the expected value.") | ||||
|  | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     { | ||||
|                         "new": { | ||||
|                             "direct_members": [hamlet.id], | ||||
|                             "direct_subgroups": [moderators_system_group.id], | ||||
|                         }, | ||||
|                         "old": hamletcharacters_group.id, | ||||
|                     } | ||||
|                 ).decode() | ||||
|             }, | ||||
|         ) | ||||
|         self.assert_json_success(result) | ||||
|         stream = get_stream("stream_name1", realm) | ||||
|         self.assertEqual(list(stream.can_remove_subscribers_group.direct_members.all()), [hamlet]) | ||||
|         self.assertEqual( | ||||
|             list(stream.can_remove_subscribers_group.direct_subgroups.all()), | ||||
|             [moderators_system_group], | ||||
|         ) | ||||
|  | ||||
|         internet_group = NamedUserGroup.objects.get( | ||||
| @@ -2301,7 +2376,7 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         ) | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(internet_group.id).decode()}, | ||||
|             {"can_remove_subscribers_group": orjson.dumps({"new": internet_group.id}).decode()}, | ||||
|         ) | ||||
|         self.assert_json_error( | ||||
|             result, | ||||
| @@ -2313,7 +2388,7 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         ) | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(owners_group.id).decode()}, | ||||
|             {"can_remove_subscribers_group": orjson.dumps({"new": owners_group.id}).decode()}, | ||||
|         ) | ||||
|         self.assert_json_error( | ||||
|             result, | ||||
| @@ -2325,7 +2400,7 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         ) | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(nobody_group.id).decode()}, | ||||
|             {"can_remove_subscribers_group": orjson.dumps({"new": nobody_group.id}).decode()}, | ||||
|         ) | ||||
|         self.assert_json_error( | ||||
|             result, | ||||
| @@ -2337,14 +2412,22 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         stream = self.make_stream("stream_name2", invite_only=True) | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(moderators_system_group.id).decode()}, | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     {"new": moderators_system_group.id} | ||||
|                 ).decode() | ||||
|             }, | ||||
|         ) | ||||
|         self.assert_json_error(result, "Invalid channel ID") | ||||
|  | ||||
|         self.subscribe(user_profile, "stream_name2") | ||||
|         result = self.client_patch( | ||||
|             f"/json/streams/{stream.id}", | ||||
|             {"can_remove_subscribers_group": orjson.dumps(moderators_system_group.id).decode()}, | ||||
|             { | ||||
|                 "can_remove_subscribers_group": orjson.dumps( | ||||
|                     {"new": moderators_system_group.id} | ||||
|                 ).decode() | ||||
|             }, | ||||
|         ) | ||||
|         self.assert_json_success(result) | ||||
|         stream = get_stream("stream_name2", realm) | ||||
| @@ -2676,7 +2759,7 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         are on. | ||||
|         """ | ||||
|         result = self.attempt_unsubscribe_of_principal( | ||||
|             query_count=17, | ||||
|             query_count=18, | ||||
|             target_users=[self.example_user("cordelia")], | ||||
|             is_realm_admin=True, | ||||
|             is_subbed=True, | ||||
| @@ -2693,7 +2776,7 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         streams you aren't on. | ||||
|         """ | ||||
|         result = self.attempt_unsubscribe_of_principal( | ||||
|             query_count=17, | ||||
|             query_count=18, | ||||
|             target_users=[self.example_user("cordelia")], | ||||
|             is_realm_admin=True, | ||||
|             is_subbed=False, | ||||
| @@ -2863,6 +2946,17 @@ class StreamAdminTest(ZulipTestCase): | ||||
|         self.subscribe(self.example_user("shiva"), stream.name) | ||||
|         check_unsubscribing_user(self.example_user("shiva"), leadership_group) | ||||
|  | ||||
|         # Test changing setting to anonymous group. | ||||
|         setting_group = self.create_or_update_anonymous_group_for_setting( | ||||
|             [hamlet], | ||||
|             [leadership_group], | ||||
|         ) | ||||
|         check_unsubscribing_user(self.example_user("othello"), setting_group, expect_fail=True) | ||||
|         check_unsubscribing_user(self.example_user("desdemona"), setting_group, expect_fail=True) | ||||
|         check_unsubscribing_user(self.example_user("hamlet"), setting_group) | ||||
|         check_unsubscribing_user(self.example_user("iago"), setting_group) | ||||
|         check_unsubscribing_user(self.example_user("shiva"), setting_group) | ||||
|  | ||||
|     def test_remove_invalid_user(self) -> None: | ||||
|         """ | ||||
|         Trying to unsubscribe an invalid user from a stream fails gracefully. | ||||
| @@ -4707,7 +4801,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|         streams_to_sub = ["multi_user_stream"] | ||||
|         with ( | ||||
|             self.capture_send_event_calls(expected_num_events=5) as events, | ||||
|             self.assert_database_query_count(37), | ||||
|             self.assert_database_query_count(40), | ||||
|         ): | ||||
|             self.common_subscribe_to_streams( | ||||
|                 self.test_user, | ||||
| @@ -4733,7 +4827,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|         # Now add ourselves | ||||
|         with ( | ||||
|             self.capture_send_event_calls(expected_num_events=2) as events, | ||||
|             self.assert_database_query_count(14), | ||||
|             self.assert_database_query_count(16), | ||||
|         ): | ||||
|             self.common_subscribe_to_streams( | ||||
|                 self.test_user, | ||||
| @@ -5027,7 +5121,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|         # Sends 3 peer-remove events, 2 unsubscribe events | ||||
|         # and 2 stream delete events for private streams. | ||||
|         with ( | ||||
|             self.assert_database_query_count(16), | ||||
|             self.assert_database_query_count(17), | ||||
|             self.assert_memcached_count(3), | ||||
|             self.capture_send_event_calls(expected_num_events=7) as events, | ||||
|         ): | ||||
| @@ -5098,7 +5192,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|         # Verify that peer_event events are never sent in Zephyr | ||||
|         # realm. This does generate stream creation events from | ||||
|         # send_stream_creation_events_for_previously_inaccessible_streams. | ||||
|         with self.assert_database_query_count(num_streams + 11): | ||||
|         with self.assert_database_query_count(num_streams + 13): | ||||
|             with self.capture_send_event_calls(expected_num_events=num_streams + 1) as events: | ||||
|                 self.common_subscribe_to_streams( | ||||
|                     mit_user, | ||||
| @@ -5179,7 +5273,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|         test_user_ids = [user.id for user in test_users] | ||||
|  | ||||
|         with ( | ||||
|             self.assert_database_query_count(16), | ||||
|             self.assert_database_query_count(18), | ||||
|             self.assert_memcached_count(3), | ||||
|             mock.patch("zerver.views.streams.send_messages_for_new_subscribers"), | ||||
|         ): | ||||
| @@ -5547,7 +5641,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|         ] | ||||
|  | ||||
|         # Test creating a public stream when realm does not have a notification stream. | ||||
|         with self.assert_database_query_count(37): | ||||
|         with self.assert_database_query_count(40): | ||||
|             self.common_subscribe_to_streams( | ||||
|                 self.test_user, | ||||
|                 [new_streams[0]], | ||||
| @@ -5555,7 +5649,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|             ) | ||||
|  | ||||
|         # Test creating private stream. | ||||
|         with self.assert_database_query_count(39): | ||||
|         with self.assert_database_query_count(42): | ||||
|             self.common_subscribe_to_streams( | ||||
|                 self.test_user, | ||||
|                 [new_streams[1]], | ||||
| @@ -5567,7 +5661,7 @@ class SubscriptionAPITest(ZulipTestCase): | ||||
|         new_stream_announcements_stream = get_stream(self.streams[0], self.test_realm) | ||||
|         self.test_realm.new_stream_announcements_stream_id = new_stream_announcements_stream.id | ||||
|         self.test_realm.save() | ||||
|         with self.assert_database_query_count(48): | ||||
|         with self.assert_database_query_count(51): | ||||
|             self.common_subscribe_to_streams( | ||||
|                 self.test_user, | ||||
|                 [new_streams[2]], | ||||
| @@ -6047,7 +6141,7 @@ class GetSubscribersTest(ZulipTestCase): | ||||
|             polonius.id, | ||||
|         ] | ||||
|  | ||||
|         with self.assert_database_query_count(43): | ||||
|         with self.assert_database_query_count(45): | ||||
|             self.common_subscribe_to_streams( | ||||
|                 self.user_profile, | ||||
|                 streams, | ||||
| @@ -6095,7 +6189,7 @@ class GetSubscribersTest(ZulipTestCase): | ||||
|         for user in [cordelia, othello, polonius]: | ||||
|             self.assert_user_got_subscription_notification(user, msg) | ||||
|  | ||||
|         with self.assert_database_query_count(4): | ||||
|         with self.assert_database_query_count(5): | ||||
|             subscribed_streams, _ = gather_subscriptions( | ||||
|                 self.user_profile, include_subscribers=True | ||||
|             ) | ||||
| @@ -6170,7 +6264,7 @@ class GetSubscribersTest(ZulipTestCase): | ||||
|         create_private_streams() | ||||
|  | ||||
|         def get_never_subscribed() -> list[NeverSubscribedStreamDict]: | ||||
|             with self.assert_database_query_count(4): | ||||
|             with self.assert_database_query_count(5): | ||||
|                 sub_data = gather_subscriptions_helper(self.user_profile) | ||||
|                 self.verify_sub_fields(sub_data) | ||||
|             never_subscribed = sub_data.never_subscribed | ||||
| @@ -6367,7 +6461,7 @@ class GetSubscribersTest(ZulipTestCase): | ||||
|             subdomain="zephyr", | ||||
|         ) | ||||
|  | ||||
|         with self.assert_database_query_count(3): | ||||
|         with self.assert_database_query_count(4): | ||||
|             subscribed_streams, _ = gather_subscriptions(mit_user_profile, include_subscribers=True) | ||||
|  | ||||
|         self.assertGreaterEqual(len(subscribed_streams), 2) | ||||
|   | ||||
| @@ -74,7 +74,14 @@ from zerver.lib.topic import ( | ||||
| ) | ||||
| from zerver.lib.typed_endpoint import ApiParamConfig, PathOnly, typed_endpoint | ||||
| from zerver.lib.typed_endpoint_validators import check_color, check_int_in_validator | ||||
| from zerver.lib.user_groups import access_user_group_for_setting | ||||
| from zerver.lib.types import AnonymousSettingGroupDict | ||||
| from zerver.lib.user_groups import ( | ||||
|     GroupSettingChangeRequest, | ||||
|     access_user_group_for_setting, | ||||
|     get_group_setting_value_for_api, | ||||
|     parse_group_setting_value, | ||||
|     validate_group_setting_value_change, | ||||
| ) | ||||
| from zerver.lib.users import bulk_access_users_by_email, bulk_access_users_by_id | ||||
| from zerver.lib.utils import assert_is_not_none | ||||
| from zerver.models import NamedUserGroup, Realm, Stream, UserProfile | ||||
| @@ -249,9 +256,7 @@ def update_stream_backend( | ||||
|     is_web_public: Json[bool] | None = None, | ||||
|     new_name: str | None = None, | ||||
|     message_retention_days: Json[str] | Json[int] | None = None, | ||||
|     can_remove_subscribers_group_id: Annotated[ | ||||
|         Json[int | None], ApiParamConfig("can_remove_subscribers_group") | ||||
|     ] = None, | ||||
|     can_remove_subscribers_group: Json[GroupSettingChangeRequest] | None = None, | ||||
| ) -> HttpResponse: | ||||
|     # We allow realm administrators to update the stream name and | ||||
|     # description even for private streams. | ||||
| @@ -379,28 +384,42 @@ def update_stream_backend( | ||||
|  | ||||
|     for setting_name, permission_configuration in Stream.stream_permission_group_settings.items(): | ||||
|         request_settings_dict = locals() | ||||
|         setting_group_id_name = permission_configuration.id_field_name | ||||
|  | ||||
|         if setting_group_id_name not in request_settings_dict:  # nocoverage | ||||
|         assert setting_name in request_settings_dict | ||||
|         if request_settings_dict[setting_name] is None: | ||||
|             continue | ||||
|  | ||||
|         if request_settings_dict[setting_group_id_name] is not None and request_settings_dict[ | ||||
|             setting_group_id_name | ||||
|         ] != getattr(stream, setting_group_id_name): | ||||
|         setting_value = request_settings_dict[setting_name] | ||||
|         new_setting_value = parse_group_setting_value(setting_value.new, setting_name) | ||||
|  | ||||
|         expected_current_setting_value = None | ||||
|         if setting_value.old is not None: | ||||
|             expected_current_setting_value = parse_group_setting_value( | ||||
|                 setting_value.old, setting_name | ||||
|             ) | ||||
|  | ||||
|         current_value = getattr(stream, setting_name) | ||||
|         current_setting_api_value = get_group_setting_value_for_api(current_value) | ||||
|  | ||||
|         if validate_group_setting_value_change( | ||||
|             current_setting_api_value, new_setting_value, expected_current_setting_value | ||||
|         ): | ||||
|             if sub is None and stream.invite_only: | ||||
|                 # Admins cannot change this setting for unsubscribed private streams. | ||||
|                 raise JsonableError(_("Invalid channel ID")) | ||||
|  | ||||
|             user_group_id = request_settings_dict[setting_group_id_name] | ||||
|             with transaction.atomic(durable=True): | ||||
|                 user_group = access_user_group_for_setting( | ||||
|                     user_group_id, | ||||
|                     new_setting_value, | ||||
|                     user_profile, | ||||
|                     setting_name=setting_name, | ||||
|                     permission_configuration=permission_configuration, | ||||
|                     current_setting_value=current_value, | ||||
|                 ) | ||||
|                 do_change_stream_group_based_setting( | ||||
|                     stream, setting_name, user_group, acting_user=user_profile | ||||
|                     stream, | ||||
|                     setting_name, | ||||
|                     user_group, | ||||
|                     old_setting_api_value=current_setting_api_value, | ||||
|                     acting_user=user_profile, | ||||
|                 ) | ||||
|  | ||||
|     return json_success(request) | ||||
| @@ -558,9 +577,7 @@ def add_subscriptions_backend( | ||||
|     ] = Stream.STREAM_POST_POLICY_EVERYONE, | ||||
|     history_public_to_subscribers: Json[bool] | None = None, | ||||
|     message_retention_days: Json[str] | Json[int] = RETENTION_DEFAULT, | ||||
|     can_remove_subscribers_group_id: Annotated[ | ||||
|         Json[int | None], ApiParamConfig("can_remove_subscribers_group") | ||||
|     ] = None, | ||||
|     can_remove_subscribers_group: Json[int | AnonymousSettingGroupDict] | None = None, | ||||
|     announce: Json[bool] = False, | ||||
|     principals: Json[list[str] | list[int]] | None = None, | ||||
|     authorization_errors_fatal: Json[bool] = True, | ||||
| @@ -572,12 +589,15 @@ def add_subscriptions_backend( | ||||
|     if principals is None: | ||||
|         principals = [] | ||||
|  | ||||
|     if can_remove_subscribers_group_id is not None: | ||||
|     if can_remove_subscribers_group is not None: | ||||
|         setting_value = parse_group_setting_value( | ||||
|             can_remove_subscribers_group, "can_remove_subscribers_group" | ||||
|         ) | ||||
|         permission_configuration = Stream.stream_permission_group_settings[ | ||||
|             "can_remove_subscribers_group" | ||||
|         ] | ||||
|         can_remove_subscribers_group = access_user_group_for_setting( | ||||
|             can_remove_subscribers_group_id, | ||||
|         can_remove_subscribers_group_value = access_user_group_for_setting( | ||||
|             setting_value, | ||||
|             user_profile, | ||||
|             setting_name="can_remove_subscribers_group", | ||||
|             permission_configuration=permission_configuration, | ||||
| @@ -586,7 +606,7 @@ def add_subscriptions_backend( | ||||
|         can_remove_subscribers_group_default_name = Stream.stream_permission_group_settings[ | ||||
|             "can_remove_subscribers_group" | ||||
|         ].default_group_name | ||||
|         can_remove_subscribers_group = NamedUserGroup.objects.get( | ||||
|         can_remove_subscribers_group_value = NamedUserGroup.objects.get( | ||||
|             name=can_remove_subscribers_group_default_name, | ||||
|             realm=user_profile.realm, | ||||
|             is_system_group=True, | ||||
| @@ -612,7 +632,7 @@ def add_subscriptions_backend( | ||||
|         stream_dict_copy["message_retention_days"] = parse_message_retention_days( | ||||
|             message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP | ||||
|         ) | ||||
|         stream_dict_copy["can_remove_subscribers_group"] = can_remove_subscribers_group | ||||
|         stream_dict_copy["can_remove_subscribers_group"] = can_remove_subscribers_group_value | ||||
|  | ||||
|         stream_dicts.append(stream_dict_copy) | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user