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:
Shubham Padia 2024-10-24 19:50:49 +00:00 committed by Tim Abbott
parent 005a27a9cf
commit b6ebf143cc
14 changed files with 458 additions and 133 deletions

View File

@ -20,6 +20,18 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0 ## 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** **Feature level 319**
* [Markdown message * [Markdown message

View File

@ -34,7 +34,9 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # 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 # 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 # only when going from an old version of the code to a newer version. Bump

View File

@ -40,13 +40,18 @@ from zerver.lib.streams import (
can_access_stream_user_ids, can_access_stream_user_ids,
check_basic_stream_access, check_basic_stream_access,
get_occupied_streams, get_occupied_streams,
get_setting_values_for_group_settings,
get_stream_permission_policy_name, get_stream_permission_policy_name,
render_stream_description, render_stream_description,
send_stream_creation_event, send_stream_creation_event,
stream_to_dict, stream_to_dict,
) )
from zerver.lib.subscription_info import get_subscribers_query 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 ( from zerver.lib.users import (
get_subscribers_of_target_user_subscriptions, get_subscribers_of_target_user_subscriptions,
get_users_involved_in_dms_with_target_users, get_users_involved_in_dms_with_target_users,
@ -349,13 +354,21 @@ def send_subscription_add_events(
subscribers = list(subscriber_dict[stream.id]) subscribers = list(subscriber_dict[stream.id])
stream_subscribers_dict[stream.id] = subscribers 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(): for user_id, sub_infos in info_by_user.items():
sub_dicts: list[APISubscriptionDict] = [] sub_dicts: list[APISubscriptionDict] = []
for sub_info in sub_infos: for sub_info in sub_infos:
stream = sub_info.stream stream = sub_info.stream
stream_subscribers = stream_subscribers_dict[stream.id] stream_subscribers = stream_subscribers_dict[stream.id]
subscription = sub_info.sub 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 # This is verbose as we cannot unpack existing TypedDict
# to initialize another TypedDict while making mypy happy. # to initialize another TypedDict while making mypy happy.
# https://github.com/python/mypy/issues/5382 # 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()) stream_ids = set(altered_user_dict.keys())
recent_traffic = get_streams_traffic(stream_ids, realm) 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(): for stream_id, stream_users_ids in altered_user_dict.items():
stream = stream_dict[stream_id] 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) notify_user_ids = list(stream_users_ids & altered_guests)
if notify_user_ids: 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( def send_peer_subscriber_events(
@ -1561,16 +1584,30 @@ def do_change_stream_group_based_setting(
setting_name: str, setting_name: str,
user_group: UserGroup, user_group: UserGroup,
*, *,
old_setting_api_value: int | AnonymousSettingGroupDict | None = None,
acting_user: UserProfile | None = None, acting_user: UserProfile | None = None,
) -> None: ) -> None:
old_user_group = getattr(stream, setting_name) 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) setattr(stream, setting_name, user_group)
stream.save() 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( RealmAuditLog.objects.create(
realm=stream.realm, realm=stream.realm,
acting_user=acting_user, acting_user=acting_user,
@ -1578,8 +1615,12 @@ def do_change_stream_group_based_setting(
event_type=AuditLogEventType.CHANNEL_GROUP_BASED_SETTING_CHANGED, event_type=AuditLogEventType.CHANNEL_GROUP_BASED_SETTING_CHANGED,
event_time=timezone_now(), event_time=timezone_now(),
extra_data={ extra_data={
RealmAuditLog.OLD_VALUE: old_user_group_id, RealmAuditLog.OLD_VALUE: get_group_setting_value_for_audit_log_data(
RealmAuditLog.NEW_VALUE: user_group.id, old_setting_api_value
),
RealmAuditLog.NEW_VALUE: get_group_setting_value_for_audit_log_data(
new_setting_api_value
),
"property": setting_name, "property": setting_name,
}, },
) )
@ -1587,7 +1628,7 @@ def do_change_stream_group_based_setting(
op="update", op="update",
type="stream", type="stream",
property=setting_name, property=setting_name,
value=user_group.id, value=new_setting_api_value,
stream_id=stream.id, stream_id=stream.id,
name=stream.name, name=stream.name,
) )

View File

@ -45,13 +45,26 @@ from zerver.lib.data_types import (
make_checker, make_checker,
) )
from zerver.lib.topic import ORIG_TOPIC, TOPIC_LINKS, TOPIC_NAME 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 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 # These fields are used for "stream" events, and are included in the
# larger "subscription" events that also contain personal settings. # larger "subscription" events that also contain personal settings.
default_stream_fields = [ default_stream_fields = [
("is_archived", bool), ("is_archived", bool),
("can_remove_subscribers_group", int), ("can_remove_subscribers_group", group_setting_type),
("creator_id", OptionalType(int)), ("creator_id", OptionalType(int)),
("date_created", int), ("date_created", int),
("description", str), ("description", str),
@ -103,6 +116,12 @@ optional_value_type = UnionType(
bool, bool,
int, int,
str, str,
DictType(
required_keys=[
("direct_members", ListType(int)),
("direct_subgroups", ListType(int)),
]
),
Equals(None), 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( group_setting_update_data_type = DictType(
required_keys=[], required_keys=[],
optional_keys=[ optional_keys=[
@ -1442,7 +1449,7 @@ def check_stream_update(
assert value in Stream.STREAM_POST_POLICY_TYPES assert value in Stream.STREAM_POST_POLICY_TYPES
elif prop == "can_remove_subscribers_group": elif prop == "can_remove_subscribers_group":
assert extra_keys == set() assert extra_keys == set()
assert isinstance(value, int) assert isinstance(value, int | AnonymousSettingGroupDict)
elif prop == "first_message_id": elif prop == "first_message_id":
assert extra_keys == set() assert extra_keys == set()
assert isinstance(value, int) assert isinstance(value, int)

View File

@ -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.stream_traffic import get_average_weekly_stream_traffic, get_streams_traffic
from zerver.lib.string_validation import check_stream_name from zerver.lib.string_validation import check_stream_name
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.types import APIStreamDict from zerver.lib.types import AnonymousSettingGroupDict, APIStreamDict
from zerver.lib.user_groups import user_has_permission_for_group_setting from zerver.lib.user_groups import (
get_group_setting_value_for_api,
user_has_permission_for_group_setting,
)
from zerver.models import ( from zerver.models import (
DefaultStreamGroup, DefaultStreamGroup,
GroupGroupMembership,
NamedUserGroup, NamedUserGroup,
Realm, Realm,
RealmAuditLog, RealmAuditLog,
@ -32,6 +36,7 @@ from zerver.models import (
Stream, Stream,
Subscription, Subscription,
UserGroup, UserGroup,
UserGroupMembership,
UserProfile, UserProfile,
) )
from zerver.models.groups import SystemGroups from zerver.models.groups import SystemGroups
@ -123,8 +128,13 @@ def send_stream_creation_event(
stream: Stream, stream: Stream,
user_ids: list[int], user_ids: list[int],
recent_traffic: dict[int, int] | None = None, recent_traffic: dict[int, int] | None = None,
setting_groups_dict: dict[int, int | AnonymousSettingGroupDict] | None = 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) send_event_on_commit(realm, event, user_ids)
@ -870,7 +880,11 @@ def get_occupied_streams(realm: Realm) -> QuerySet[Stream]:
return occupied_streams 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: if recent_traffic is not None:
stream_weekly_traffic = get_average_weekly_stream_traffic( stream_weekly_traffic = get_average_weekly_stream_traffic(
stream.id, stream.date_created, recent_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. # passing stream data to spectators.
stream_weekly_traffic = None 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( return APIStreamDict(
is_archived=stream.deactivated, 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, creator_id=stream.creator_id,
date_created=datetime_to_timestamp(stream.date_created), date_created=datetime_to_timestamp(stream.date_created),
description=stream.description, description=stream.description,
@ -978,6 +999,38 @@ def get_streams_for_user(
return list(streams) 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( def do_get_streams(
user_profile: UserProfile, user_profile: UserProfile,
include_public: bool = True, include_public: bool = True,
@ -1003,14 +1056,22 @@ def do_get_streams(
stream_ids = {stream.id for stream in streams} stream_ids = {stream.id for stream in streams}
recent_traffic = get_streams_traffic(stream_ids, user_profile.realm) 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_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: if include_default:
default_stream_ids = get_default_stream_ids_for_realm(user_profile.realm_id) default_stream_ids = get_default_stream_ids_for_realm(user_profile.realm_id)
for stream in stream_dicts: for stream_dict in stream_dicts:
stream["is_default"] = stream["stream_id"] in default_stream_ids stream_dict["is_default"] = stream_dict["stream_id"] in default_stream_ids
return stream_dicts return stream_dicts

View File

@ -16,9 +16,14 @@ from zerver.lib.stream_subscription import (
get_stream_subscriptions_for_user, get_stream_subscriptions_for_user,
) )
from zerver.lib.stream_traffic import get_average_weekly_stream_traffic, get_streams_traffic 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.timestamp import datetime_to_timestamp
from zerver.lib.types import ( from zerver.lib.types import (
AnonymousSettingGroupDict,
APIStreamDict, APIStreamDict,
NeverSubscribedStreamDict, NeverSubscribedStreamDict,
RawStreamDict, RawStreamDict,
@ -26,6 +31,7 @@ from zerver.lib.types import (
SubscriptionInfo, SubscriptionInfo,
SubscriptionStreamDict, SubscriptionStreamDict,
) )
from zerver.lib.user_groups import get_group_setting_value_for_api
from zerver.models import Realm, Stream, Subscription, UserProfile from zerver.models import Realm, Stream, Subscription, UserProfile
from zerver.models.streams import get_all_streams 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): for stream in get_web_public_streams_queryset(realm):
# Add Stream fields. # Add Stream fields.
is_archived = stream.deactivated 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 creator_id = stream.creator_id
date_created = datetime_to_timestamp(stream.date_created) date_created = datetime_to_timestamp(stream.date_created)
description = stream.description description = stream.description
@ -76,7 +84,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo:
sub = SubscriptionStreamDict( sub = SubscriptionStreamDict(
is_archived=is_archived, is_archived=is_archived,
audible_notifications=audible_notifications, audible_notifications=audible_notifications,
can_remove_subscribers_group=can_remove_subscribers_group_id, can_remove_subscribers_group=can_remove_subscribers_group,
color=color, color=color,
creator_id=creator_id, creator_id=creator_id,
date_created=date_created, date_created=date_created,
@ -118,7 +126,9 @@ def build_unsubscribed_sub_from_stream_dict(
def build_stream_api_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: ) -> APIStreamDict:
# Add a few computed fields not directly from the data models. # Add a few computed fields not directly from the data models.
if recent_traffic is not None: if recent_traffic is not None:
@ -133,9 +143,13 @@ def build_stream_api_dict(
# migration. # migration.
is_announcement_only = raw_stream_dict["stream_post_policy"] == Stream.STREAM_POST_POLICY_ADMINS 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( return APIStreamDict(
is_archived=raw_stream_dict["deactivated"], 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"], creator_id=raw_stream_dict["creator_id"],
date_created=datetime_to_timestamp(raw_stream_dict["date_created"]), date_created=datetime_to_timestamp(raw_stream_dict["date_created"]),
description=raw_stream_dict["description"], 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} 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] = ( sub_dicts_query: Iterable[RawSubscriptionDict] = (
get_stream_subscriptions_for_user(user_profile) get_stream_subscriptions_for_user(user_profile)
.values( .values(
@ -505,7 +526,9 @@ def gather_subscriptions_helper(
stream_id = get_stream_id(sub_dict) stream_id = get_stream_id(sub_dict)
sub_unsub_stream_ids.add(stream_id) sub_unsub_stream_ids.add(stream_id)
raw_stream_dict = all_streams_map[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( stream_dict = build_stream_dict_for_sub(
user=user_profile, user=user_profile,
sub_dict=sub_dict, sub_dict=sub_dict,

View File

@ -191,7 +191,7 @@ class SubscriptionStreamDict(TypedDict):
""" """
audible_notifications: bool | None audible_notifications: bool | None
can_remove_subscribers_group: int can_remove_subscribers_group: int | AnonymousSettingGroupDict
color: str color: str
creator_id: int | None creator_id: int | None
date_created: int date_created: int
@ -245,7 +245,7 @@ class DefaultStreamDict(TypedDict):
""" """
is_archived: bool is_archived: bool
can_remove_subscribers_group: int can_remove_subscribers_group: int | AnonymousSettingGroupDict
creator_id: int | None creator_id: int | None
date_created: int date_created: int
description: str description: str

View File

@ -143,7 +143,7 @@ class Stream(models.Model):
stream_permission_group_settings = { stream_permission_group_settings = {
"can_remove_subscribers_group": GroupPermissionSetting( "can_remove_subscribers_group": GroupPermissionSetting(
require_system_group=True, require_system_group=False,
allow_internet_group=False, allow_internet_group=False,
allow_owners_group=False, allow_owners_group=False,
allow_nobody_group=False, allow_nobody_group=False,

View File

@ -1459,7 +1459,33 @@ paths:
value: value:
description: | description: |
The new value of the changed property. 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: 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: integer
- type: boolean - type: boolean
- type: string - type: string
@ -10364,7 +10390,7 @@ paths:
message_retention_days: message_retention_days:
$ref: "#/components/schemas/MessageRetentionDays" $ref: "#/components/schemas/MessageRetentionDays"
can_remove_subscribers_group: can_remove_subscribers_group:
$ref: "#/components/schemas/CanRemoveSubscribersGroupId" $ref: "#/components/schemas/CanRemoveSubscribersGroup"
required: required:
- subscriptions - subscriptions
encoding: encoding:
@ -19958,7 +19984,43 @@ paths:
message_retention_days: message_retention_days:
$ref: "#/components/schemas/MessageRetentionDays" $ref: "#/components/schemas/MessageRetentionDays"
can_remove_subscribers_group: 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: encoding:
is_private: is_private:
contentType: application/json contentType: application/json
@ -21819,16 +21881,7 @@ components:
**Changes**: Deprecated in Zulip 3.0 (feature level 1). Clients **Changes**: Deprecated in Zulip 3.0 (feature level 1). Clients
should use `stream_post_policy` instead. should use `stream_post_policy` instead.
can_remove_subscribers_group: can_remove_subscribers_group:
type: integer $ref: "#/components/schemas/CanRemoveSubscribersGroup"
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).
BasicBot: BasicBot:
allOf: allOf:
- $ref: "#/components/schemas/BasicBotBase" - $ref: "#/components/schemas/BasicBotBase"
@ -22835,16 +22888,7 @@ components:
If `null`, the channel was recently created and there is If `null`, the channel was recently created and there is
insufficient data to estimate the average traffic. insufficient data to estimate the average traffic.
can_remove_subscribers_group: can_remove_subscribers_group:
type: integer $ref: "#/components/schemas/CanRemoveSubscribersGroup"
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).
is_archived: is_archived:
type: boolean type: boolean
description: | description: |
@ -24515,25 +24559,26 @@ components:
- type: string - type: string
- type: integer - type: integer
example: "20" example: "20"
CanRemoveSubscribersGroupId: CanRemoveSubscribersGroup:
description: | allOf:
ID of the [user group](/api/get-user-groups) whose members are - $ref: "#/components/schemas/GroupSettingValue"
allowed to unsubscribe others from the channel. Note that a user - description: |
who is a member of the specified user group must also [have A [group-setting value][setting-values] defining the set of users
access](/help/channel-permissions) to the channel in order to who have permission to remove subscribers from this channel.
unsubscribe others.
This setting can currently only be set to user groups that are Note that a user who is a member of the specified user group must
system groups, except for the system groups named also [have access](/help/channel-permissions) to the channel in
`"role:internet"` and `"role:owners"`. order to unsubscribe others.
**Changes**: Before Zulip 8.0 (feature level 197), **Changes**: Prior to Zulip 10.0 (feature level 320), this value was
the `can_remove_subscribers_group` setting always the integer ID of a system group.
was named `can_remove_subscribers_group_id`.
New in Zulip 7.0 (feature level 161). Before Zulip 8.0 (feature level 197), the `can_remove_subscribers_group`
type: integer setting was named `can_remove_subscribers_group_id`.
example: 20
New in Zulip 6.0 (feature level 142).
[setting-values]: /api/group-setting-values
LinkifierPattern: LinkifierPattern:
description: | description: |
The [Python regular The [Python regular

View File

@ -1192,7 +1192,7 @@ class FetchQueriesTest(ZulipTestCase):
realm = get_realm_with_settings(realm_id=user.realm_id) realm = get_realm_with_settings(realm_id=user.realm_id)
with ( with (
self.assert_database_query_count(40), self.assert_database_query_count(42),
mock.patch("zerver.lib.events.always_want") as want_mock, mock.patch("zerver.lib.events.always_want") as want_mock,
): ):
fetch_initial_state_data(user, realm=realm) fetch_initial_state_data(user, realm=realm)
@ -1224,9 +1224,9 @@ class FetchQueriesTest(ZulipTestCase):
saved_snippets=1, saved_snippets=1,
scheduled_messages=1, scheduled_messages=1,
starred_messages=1, starred_messages=1,
stream=3, stream=4,
stop_words=0, stop_words=0,
subscription=4, subscription=5,
update_display_settings=0, update_display_settings=0,
update_global_notifications=0, update_global_notifications=0,
update_message_flags=5, update_message_flags=5,

View File

@ -4538,6 +4538,26 @@ class SubscribeActionTest(BaseAction):
acting_user=self.example_user("hamlet"), acting_user=self.example_user("hamlet"),
) )
check_stream_update("events[0]", events[0]) 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 # 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) stream = self.make_stream("private", self.user_profile.realm, invite_only=True)

View File

@ -271,7 +271,7 @@ class HomeTest(ZulipTestCase):
# Verify succeeds once logged-in # Verify succeeds once logged-in
with ( with (
self.assert_database_query_count(51), self.assert_database_query_count(52),
patch("zerver.lib.cache.cache_set") as cache_mock, patch("zerver.lib.cache.cache_set") as cache_mock,
): ):
result = self._get_home_page(stream="Denmark") 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. # Verify number of queries for Realm admin isn't much higher than for normal users.
self.login("iago") self.login("iago")
with ( with (
self.assert_database_query_count(51), self.assert_database_query_count(52),
patch("zerver.lib.cache.cache_set") as cache_mock, patch("zerver.lib.cache.cache_set") as cache_mock,
): ):
result = self._get_home_page() result = self._get_home_page()
@ -608,7 +608,7 @@ class HomeTest(ZulipTestCase):
self._get_home_page() self._get_home_page()
# Then for the second page load, measure the number of queries. # 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() result = self._get_home_page()
# Do a sanity check that our new streams were in the payload. # Do a sanity check that our new streams were in the payload.

View File

@ -570,10 +570,31 @@ class TestCreateStreams(ZulipTestCase):
allow_fail=True, allow_fail=True,
subdomain="zulip", subdomain="zulip",
) )
self.assert_json_error( self.assert_json_success(result)
result, "'can_remove_subscribers_group' must be a system user group." 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( internet_group = NamedUserGroup.objects.get(
name="role:internet", is_system_group=True, realm=realm name="role:internet", is_system_group=True, realm=realm
) )
@ -2273,14 +2294,22 @@ class StreamAdminTest(ZulipTestCase):
self.login("shiva") self.login("shiva")
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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.assert_json_error(result, "Must be an organization administrator")
self.login("iago") self.login("iago")
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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) self.assert_json_success(result)
stream = get_stream("stream_name1", realm) stream = get_stream("stream_name1", realm)
@ -2290,10 +2319,56 @@ class StreamAdminTest(ZulipTestCase):
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm) hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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( self.assert_json_success(result)
result, "'can_remove_subscribers_group' must be a system user group." 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( internet_group = NamedUserGroup.objects.get(
@ -2301,7 +2376,7 @@ class StreamAdminTest(ZulipTestCase):
) )
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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( self.assert_json_error(
result, result,
@ -2313,7 +2388,7 @@ class StreamAdminTest(ZulipTestCase):
) )
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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( self.assert_json_error(
result, result,
@ -2325,7 +2400,7 @@ class StreamAdminTest(ZulipTestCase):
) )
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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( self.assert_json_error(
result, result,
@ -2337,14 +2412,22 @@ class StreamAdminTest(ZulipTestCase):
stream = self.make_stream("stream_name2", invite_only=True) stream = self.make_stream("stream_name2", invite_only=True)
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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.assert_json_error(result, "Invalid channel ID")
self.subscribe(user_profile, "stream_name2") self.subscribe(user_profile, "stream_name2")
result = self.client_patch( result = self.client_patch(
f"/json/streams/{stream.id}", 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) self.assert_json_success(result)
stream = get_stream("stream_name2", realm) stream = get_stream("stream_name2", realm)
@ -2676,7 +2759,7 @@ class StreamAdminTest(ZulipTestCase):
are on. are on.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=17, query_count=18,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=True, is_subbed=True,
@ -2693,7 +2776,7 @@ class StreamAdminTest(ZulipTestCase):
streams you aren't on. streams you aren't on.
""" """
result = self.attempt_unsubscribe_of_principal( result = self.attempt_unsubscribe_of_principal(
query_count=17, query_count=18,
target_users=[self.example_user("cordelia")], target_users=[self.example_user("cordelia")],
is_realm_admin=True, is_realm_admin=True,
is_subbed=False, is_subbed=False,
@ -2863,6 +2946,17 @@ class StreamAdminTest(ZulipTestCase):
self.subscribe(self.example_user("shiva"), stream.name) self.subscribe(self.example_user("shiva"), stream.name)
check_unsubscribing_user(self.example_user("shiva"), leadership_group) 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: def test_remove_invalid_user(self) -> None:
""" """
Trying to unsubscribe an invalid user from a stream fails gracefully. Trying to unsubscribe an invalid user from a stream fails gracefully.
@ -4707,7 +4801,7 @@ class SubscriptionAPITest(ZulipTestCase):
streams_to_sub = ["multi_user_stream"] streams_to_sub = ["multi_user_stream"]
with ( with (
self.capture_send_event_calls(expected_num_events=5) as events, 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.common_subscribe_to_streams(
self.test_user, self.test_user,
@ -4733,7 +4827,7 @@ class SubscriptionAPITest(ZulipTestCase):
# Now add ourselves # Now add ourselves
with ( with (
self.capture_send_event_calls(expected_num_events=2) as events, 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.common_subscribe_to_streams(
self.test_user, self.test_user,
@ -5027,7 +5121,7 @@ class SubscriptionAPITest(ZulipTestCase):
# Sends 3 peer-remove events, 2 unsubscribe events # Sends 3 peer-remove events, 2 unsubscribe events
# and 2 stream delete events for private streams. # and 2 stream delete events for private streams.
with ( with (
self.assert_database_query_count(16), self.assert_database_query_count(17),
self.assert_memcached_count(3), self.assert_memcached_count(3),
self.capture_send_event_calls(expected_num_events=7) as events, 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 # Verify that peer_event events are never sent in Zephyr
# realm. This does generate stream creation events from # realm. This does generate stream creation events from
# send_stream_creation_events_for_previously_inaccessible_streams. # 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: with self.capture_send_event_calls(expected_num_events=num_streams + 1) as events:
self.common_subscribe_to_streams( self.common_subscribe_to_streams(
mit_user, mit_user,
@ -5179,7 +5273,7 @@ class SubscriptionAPITest(ZulipTestCase):
test_user_ids = [user.id for user in test_users] test_user_ids = [user.id for user in test_users]
with ( with (
self.assert_database_query_count(16), self.assert_database_query_count(18),
self.assert_memcached_count(3), self.assert_memcached_count(3),
mock.patch("zerver.views.streams.send_messages_for_new_subscribers"), 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. # 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.common_subscribe_to_streams(
self.test_user, self.test_user,
[new_streams[0]], [new_streams[0]],
@ -5555,7 +5649,7 @@ class SubscriptionAPITest(ZulipTestCase):
) )
# Test creating private stream. # Test creating private stream.
with self.assert_database_query_count(39): with self.assert_database_query_count(42):
self.common_subscribe_to_streams( self.common_subscribe_to_streams(
self.test_user, self.test_user,
[new_streams[1]], [new_streams[1]],
@ -5567,7 +5661,7 @@ class SubscriptionAPITest(ZulipTestCase):
new_stream_announcements_stream = get_stream(self.streams[0], self.test_realm) 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.new_stream_announcements_stream_id = new_stream_announcements_stream.id
self.test_realm.save() self.test_realm.save()
with self.assert_database_query_count(48): with self.assert_database_query_count(51):
self.common_subscribe_to_streams( self.common_subscribe_to_streams(
self.test_user, self.test_user,
[new_streams[2]], [new_streams[2]],
@ -6047,7 +6141,7 @@ class GetSubscribersTest(ZulipTestCase):
polonius.id, polonius.id,
] ]
with self.assert_database_query_count(43): with self.assert_database_query_count(45):
self.common_subscribe_to_streams( self.common_subscribe_to_streams(
self.user_profile, self.user_profile,
streams, streams,
@ -6095,7 +6189,7 @@ class GetSubscribersTest(ZulipTestCase):
for user in [cordelia, othello, polonius]: for user in [cordelia, othello, polonius]:
self.assert_user_got_subscription_notification(user, msg) 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( subscribed_streams, _ = gather_subscriptions(
self.user_profile, include_subscribers=True self.user_profile, include_subscribers=True
) )
@ -6170,7 +6264,7 @@ class GetSubscribersTest(ZulipTestCase):
create_private_streams() create_private_streams()
def get_never_subscribed() -> list[NeverSubscribedStreamDict]: 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) sub_data = gather_subscriptions_helper(self.user_profile)
self.verify_sub_fields(sub_data) self.verify_sub_fields(sub_data)
never_subscribed = sub_data.never_subscribed never_subscribed = sub_data.never_subscribed
@ -6367,7 +6461,7 @@ class GetSubscribersTest(ZulipTestCase):
subdomain="zephyr", 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) subscribed_streams, _ = gather_subscriptions(mit_user_profile, include_subscribers=True)
self.assertGreaterEqual(len(subscribed_streams), 2) self.assertGreaterEqual(len(subscribed_streams), 2)

View File

@ -74,7 +74,14 @@ from zerver.lib.topic import (
) )
from zerver.lib.typed_endpoint import ApiParamConfig, PathOnly, typed_endpoint 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.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.users import bulk_access_users_by_email, bulk_access_users_by_id
from zerver.lib.utils import assert_is_not_none from zerver.lib.utils import assert_is_not_none
from zerver.models import NamedUserGroup, Realm, Stream, UserProfile from zerver.models import NamedUserGroup, Realm, Stream, UserProfile
@ -249,9 +256,7 @@ def update_stream_backend(
is_web_public: Json[bool] | None = None, is_web_public: Json[bool] | None = None,
new_name: str | None = None, new_name: str | None = None,
message_retention_days: Json[str] | Json[int] | None = None, message_retention_days: Json[str] | Json[int] | None = None,
can_remove_subscribers_group_id: Annotated[ can_remove_subscribers_group: Json[GroupSettingChangeRequest] | None = None,
Json[int | None], ApiParamConfig("can_remove_subscribers_group")
] = None,
) -> HttpResponse: ) -> HttpResponse:
# We allow realm administrators to update the stream name and # We allow realm administrators to update the stream name and
# description even for private streams. # 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(): for setting_name, permission_configuration in Stream.stream_permission_group_settings.items():
request_settings_dict = locals() request_settings_dict = locals()
setting_group_id_name = permission_configuration.id_field_name assert setting_name in request_settings_dict
if request_settings_dict[setting_name] is None:
if setting_group_id_name not in request_settings_dict: # nocoverage
continue continue
if request_settings_dict[setting_group_id_name] is not None and request_settings_dict[ setting_value = request_settings_dict[setting_name]
setting_group_id_name new_setting_value = parse_group_setting_value(setting_value.new, setting_name)
] != getattr(stream, setting_group_id_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: if sub is None and stream.invite_only:
# Admins cannot change this setting for unsubscribed private streams. # Admins cannot change this setting for unsubscribed private streams.
raise JsonableError(_("Invalid channel ID")) raise JsonableError(_("Invalid channel ID"))
user_group_id = request_settings_dict[setting_group_id_name]
with transaction.atomic(durable=True): with transaction.atomic(durable=True):
user_group = access_user_group_for_setting( user_group = access_user_group_for_setting(
user_group_id, new_setting_value,
user_profile, user_profile,
setting_name=setting_name, setting_name=setting_name,
permission_configuration=permission_configuration, permission_configuration=permission_configuration,
current_setting_value=current_value,
) )
do_change_stream_group_based_setting( 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) return json_success(request)
@ -558,9 +577,7 @@ def add_subscriptions_backend(
] = Stream.STREAM_POST_POLICY_EVERYONE, ] = Stream.STREAM_POST_POLICY_EVERYONE,
history_public_to_subscribers: Json[bool] | None = None, history_public_to_subscribers: Json[bool] | None = None,
message_retention_days: Json[str] | Json[int] = RETENTION_DEFAULT, message_retention_days: Json[str] | Json[int] = RETENTION_DEFAULT,
can_remove_subscribers_group_id: Annotated[ can_remove_subscribers_group: Json[int | AnonymousSettingGroupDict] | None = None,
Json[int | None], ApiParamConfig("can_remove_subscribers_group")
] = None,
announce: Json[bool] = False, announce: Json[bool] = False,
principals: Json[list[str] | list[int]] | None = None, principals: Json[list[str] | list[int]] | None = None,
authorization_errors_fatal: Json[bool] = True, authorization_errors_fatal: Json[bool] = True,
@ -572,12 +589,15 @@ def add_subscriptions_backend(
if principals is None: if principals is None:
principals = [] 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[ permission_configuration = Stream.stream_permission_group_settings[
"can_remove_subscribers_group" "can_remove_subscribers_group"
] ]
can_remove_subscribers_group = access_user_group_for_setting( can_remove_subscribers_group_value = access_user_group_for_setting(
can_remove_subscribers_group_id, setting_value,
user_profile, user_profile,
setting_name="can_remove_subscribers_group", setting_name="can_remove_subscribers_group",
permission_configuration=permission_configuration, 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_name = Stream.stream_permission_group_settings[
"can_remove_subscribers_group" "can_remove_subscribers_group"
].default_group_name ].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, name=can_remove_subscribers_group_default_name,
realm=user_profile.realm, realm=user_profile.realm,
is_system_group=True, is_system_group=True,
@ -612,7 +632,7 @@ def add_subscriptions_backend(
stream_dict_copy["message_retention_days"] = parse_message_retention_days( stream_dict_copy["message_retention_days"] = parse_message_retention_days(
message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP 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) stream_dicts.append(stream_dict_copy)