streams: Add can_administer_channel_group as a stream setting.

We have not added current user as the default for new channels in this
commit.
This commit is contained in:
Shubham Padia
2024-11-18 09:10:37 +00:00
committed by Tim Abbott
parent 44b498f96b
commit eb943d54a9
17 changed files with 472 additions and 15 deletions

View File

@@ -66,7 +66,6 @@ from zerver.models import (
Client,
DirectMessageGroup,
Message,
NamedUserGroup,
PreregistrationUser,
RealmAuditLog,
Recipient,
@@ -75,7 +74,6 @@ from zerver.models import (
UserProfile,
)
from zerver.models.clients import get_client
from zerver.models.groups import SystemGroups
from zerver.models.messages import Attachment
from zerver.models.realm_audit_logs import AuditLogEventType
from zerver.models.scheduled_jobs import NotificationTriggers
@@ -103,11 +101,6 @@ class AnalyticsTestCase(ZulipTestCase):
self.default_realm = do_create_realm(
string_id="realmtest", name="Realm Test", date_created=self.TIME_ZERO - 2 * self.DAY
)
self.administrators_user_group = NamedUserGroup.objects.get(
name=SystemGroups.ADMINISTRATORS,
realm=self.default_realm,
is_system_group=True,
)
# used to generate unique names in self.create_*
self.name_counter = 100

View File

@@ -20,6 +20,20 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 10.0
**Feature level 325**
* [`GET /users/me/subscriptions`](/api/get-subscriptions),
[`GET /streams`](/api/get-streams), [`GET /events`](/api/get-events),
[`POST /register`](/api/register-queue): Added `can_administer_channel_group`
which is a [group-setting value](/api/group-setting-values) describing the
set of users with permissions to administer the channel in addition to realm
admins.
* [`POST /users/me/subscriptions`](/api/subscribe),
[`PATCH /streams/{stream_id}`](/api/update-stream): Added `can_administer_channel_group`
which is a [group-setting value](/api/group-setting-values) describing the
set of users with permissions to administer the channel in addition to realm
admins.
**Feature level 324**
* [`POST /register`](/api/register-queue), [`GET /events`](/api/get-events),

View File

@@ -34,7 +34,7 @@ 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 = 324 # Last bumped for can_remove_members_group
API_FEATURE_LEVEL = 325 # Last bumped for can_administer_channel_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

View File

@@ -391,6 +391,7 @@ def send_subscription_add_events(
subscribers=stream_subscribers,
# Fields from Stream.API_FIELDS
is_archived=stream_dict["is_archived"],
can_administer_channel_group=stream_dict["can_administer_channel_group"],
can_remove_subscribers_group=stream_dict["can_remove_subscribers_group"],
creator_id=stream_dict["creator_id"],
date_created=stream_dict["date_created"],

View File

@@ -64,6 +64,7 @@ group_setting_type = UnionType(
# larger "subscription" events that also contain personal settings.
default_stream_fields = [
("is_archived", bool),
("can_administer_channel_group", group_setting_type),
("can_remove_subscribers_group", group_setting_type),
("creator_id", OptionalType(int)),
("date_created", int),

View File

@@ -76,6 +76,7 @@ class StreamDict(TypedDict, total=False):
stream_post_policy: int
history_public_to_subscribers: bool | None
message_retention_days: int | None
can_administer_channel_group: UserGroup | None
can_remove_subscribers_group: UserGroup | None
@@ -169,6 +170,7 @@ def create_stream_if_needed(
history_public_to_subscribers: bool | None = None,
stream_description: str = "",
message_retention_days: int | None = None,
can_administer_channel_group: UserGroup | None = None,
can_remove_subscribers_group: UserGroup | None = None,
acting_user: UserProfile | None = None,
setting_groups_dict: dict[int, int | AnonymousSettingGroupDict] | None = None,
@@ -273,6 +275,7 @@ def create_streams_if_needed(
history_public_to_subscribers=stream_dict.get("history_public_to_subscribers"),
stream_description=stream_dict.get("description", ""),
message_retention_days=stream_dict.get("message_retention_days", None),
can_administer_channel_group=stream_dict.get("can_administer_channel_group", None),
can_remove_subscribers_group=stream_dict.get("can_remove_subscribers_group", None),
acting_user=acting_user,
setting_groups_dict=setting_groups_dict,
@@ -950,14 +953,19 @@ def stream_to_dict(
stream_weekly_traffic = None
if setting_groups_dict is not None:
can_administer_channel_group = setting_groups_dict[stream.can_administer_channel_group_id]
can_remove_subscribers_group = setting_groups_dict[stream.can_remove_subscribers_group_id]
else:
can_administer_channel_group = get_group_setting_value_for_api(
stream.can_administer_channel_group
)
can_remove_subscribers_group = get_group_setting_value_for_api(
stream.can_remove_subscribers_group
)
return APIStreamDict(
is_archived=stream.deactivated,
can_administer_channel_group=can_administer_channel_group,
can_remove_subscribers_group=can_remove_subscribers_group,
creator_id=stream.creator_id,
date_created=datetime_to_timestamp(stream.date_created),

View File

@@ -52,6 +52,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo:
for stream in streams:
# Add Stream fields.
is_archived = stream.deactivated
can_administer_channel_group = setting_groups_dict[stream.can_administer_channel_group_id]
can_remove_subscribers_group = setting_groups_dict[stream.can_remove_subscribers_group_id]
creator_id = stream.creator_id
date_created = datetime_to_timestamp(stream.date_created)
@@ -86,6 +87,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo:
sub = SubscriptionStreamDict(
is_archived=is_archived,
audible_notifications=audible_notifications,
can_administer_channel_group=can_administer_channel_group,
can_remove_subscribers_group=can_remove_subscribers_group,
color=color,
creator_id=creator_id,
@@ -146,12 +148,16 @@ def build_stream_api_dict(
# migration.
is_announcement_only = raw_stream_dict["stream_post_policy"] == Stream.STREAM_POST_POLICY_ADMINS
can_administer_channel_group = setting_groups_dict[
raw_stream_dict["can_administer_channel_group_id"]
]
can_remove_subscribers_group = setting_groups_dict[
raw_stream_dict["can_remove_subscribers_group_id"]
]
return APIStreamDict(
is_archived=raw_stream_dict["deactivated"],
can_administer_channel_group=can_administer_channel_group,
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"]),
@@ -178,6 +184,7 @@ def build_stream_dict_for_sub(
) -> SubscriptionStreamDict:
# Handle Stream.API_FIELDS
is_archived = stream_dict["is_archived"]
can_administer_channel_group = stream_dict["can_administer_channel_group"]
can_remove_subscribers_group = stream_dict["can_remove_subscribers_group"]
creator_id = stream_dict["creator_id"]
date_created = stream_dict["date_created"]
@@ -213,6 +220,7 @@ def build_stream_dict_for_sub(
return SubscriptionStreamDict(
is_archived=is_archived,
audible_notifications=audible_notifications,
can_administer_channel_group=can_administer_channel_group,
can_remove_subscribers_group=can_remove_subscribers_group,
color=color,
creator_id=creator_id,
@@ -267,6 +275,9 @@ def build_stream_dict_for_never_sub(
else:
stream_weekly_traffic = None
can_administer_channel_group_value = setting_groups_dict[
raw_stream_dict["can_administer_channel_group_id"]
]
can_remove_subscribers_group_value = setting_groups_dict[
raw_stream_dict["can_remove_subscribers_group_id"]
]
@@ -277,6 +288,7 @@ def build_stream_dict_for_never_sub(
# Our caller may add a subscribers field.
return NeverSubscribedStreamDict(
is_archived=is_archived,
can_administer_channel_group=can_administer_channel_group_value,
can_remove_subscribers_group=can_remove_subscribers_group_value,
creator_id=creator_id,
date_created=date_created,

View File

@@ -150,6 +150,7 @@ class RawStreamDict(TypedDict):
are needed to encode the stream for the API.
"""
can_administer_channel_group_id: int
can_remove_subscribers_group_id: int
creator_id: int | None
date_created: datetime
@@ -192,6 +193,7 @@ class SubscriptionStreamDict(TypedDict):
"""
audible_notifications: bool | None
can_administer_channel_group: int | AnonymousSettingGroupDict
can_remove_subscribers_group: int | AnonymousSettingGroupDict
color: str
creator_id: int | None
@@ -222,6 +224,7 @@ class SubscriptionStreamDict(TypedDict):
class NeverSubscribedStreamDict(TypedDict):
is_archived: bool
can_administer_channel_group: int | AnonymousSettingGroupDict
can_remove_subscribers_group: int | AnonymousSettingGroupDict
creator_id: int | None
date_created: int
@@ -248,6 +251,7 @@ class DefaultStreamDict(TypedDict):
"""
is_archived: bool
can_administer_channel_group: int | AnonymousSettingGroupDict
can_remove_subscribers_group: int | AnonymousSettingGroupDict
creator_id: int | None
date_created: int

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.0.9 on 2024-11-12 04:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0635_alter_namedusergroup_can_remove_members_group"),
]
operations = [
migrations.AddField(
model_name="stream",
name="can_administer_channel_group",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -0,0 +1,58 @@
# Generated by Django 5.0.9 on 2024-11-12 05:08
from django.db import migrations, transaction
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.migrations.state import StateApps
from django.db.models import Max, Min, OuterRef
def set_default_value_for_can_administer_channel_group(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
Stream = apps.get_model("zerver", "stream")
NamedUserGroup = apps.get_model("zerver", "NamedUserGroup")
BATCH_SIZE = 1000
max_id = Stream.objects.filter(can_administer_channel_group=None).aggregate(Max("id"))[
"id__max"
]
if max_id is None:
# Do nothing if there are no channels on the server.
return
lower_bound = Stream.objects.filter(can_administer_channel_group=None).aggregate(Min("id"))[
"id__min"
]
while lower_bound <= max_id + BATCH_SIZE / 2:
upper_bound = lower_bound + BATCH_SIZE - 1
print(f"Processing batch {lower_bound} to {upper_bound} for Stream")
with transaction.atomic():
Stream.objects.filter(
id__range=(lower_bound, upper_bound),
can_administer_channel_group=None,
).update(
can_administer_channel_group=NamedUserGroup.objects.filter(
name="role:nobody",
realm_for_sharding=OuterRef("realm_id"),
is_system_group=True,
).values("pk")
)
lower_bound += BATCH_SIZE
class Migration(migrations.Migration):
atomic = False
dependencies = [
("zerver", "0636_streams_add_can_administer_channel_group"),
]
operations = [
migrations.RunPython(
set_default_value_for_can_administer_channel_group,
elidable=True,
reverse_code=migrations.RunPython.noop,
)
]

View File

@@ -0,0 +1,22 @@
# Generated by Django 5.0.9 on 2024-11-12 04:57
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("zerver", "0637_set_default_for_can_administer_channel_group"),
]
operations = [
migrations.AlterField(
model_name="stream",
name="can_administer_channel_group",
field=models.ForeignKey(
on_delete=django.db.models.deletion.RESTRICT,
related_name="+",
to="zerver.usergroup",
),
),
]

View File

@@ -121,10 +121,14 @@ class Stream(models.Model):
}
message_retention_days = models.IntegerField(null=True, default=None)
# on_delete field here is set to RESTRICT because we don't want to allow
# deleting a user group in case it is referenced by this setting.
# We are not using PROTECT since we want to allow deletion of user groups
# when realm itself is deleted.
# on_delete field for group value settings is set to RESTRICT
# because we don't want to allow deleting a user group in case it
# is referenced by the respective setting. We are not using PROTECT
# since we want to allow deletion of user groups when the realm
# itself is deleted.
can_administer_channel_group = models.ForeignKey(
UserGroup, on_delete=models.RESTRICT, related_name="+"
)
can_remove_subscribers_group = models.ForeignKey(UserGroup, on_delete=models.RESTRICT)
# The very first message ID in the stream. Used to help clients
@@ -138,6 +142,15 @@ class Stream(models.Model):
is_recently_active = models.BooleanField(default=True, db_default=True)
stream_permission_group_settings = {
"can_administer_channel_group": GroupPermissionSetting(
require_system_group=False,
allow_internet_group=False,
allow_owners_group=True,
allow_nobody_group=True,
allow_everyone_group=False,
default_group_name=SystemGroups.NOBODY,
id_field_name="can_administer_channel_group_id",
),
"can_remove_subscribers_group": GroupPermissionSetting(
require_system_group=False,
allow_internet_group=False,
@@ -188,6 +201,7 @@ class Stream(models.Model):
"name",
"rendered_description",
"stream_post_policy",
"can_administer_channel_group_id",
"can_remove_subscribers_group_id",
"is_recently_active",
]
@@ -195,6 +209,7 @@ class Stream(models.Model):
def to_dict(self) -> DefaultStreamDict:
return DefaultStreamDict(
is_archived=self.deactivated,
can_administer_channel_group=self.can_administer_channel_group_id,
can_remove_subscribers_group=self.can_remove_subscribers_group_id,
creator_id=self.creator_id,
date_created=datetime_to_timestamp(self.date_created),

View File

@@ -10420,6 +10420,8 @@ paths:
$ref: "#/components/schemas/MessageRetentionDays"
can_remove_subscribers_group:
$ref: "#/components/schemas/CanRemoveSubscribersGroup"
can_administer_channel_group:
$ref: "#/components/schemas/CanAdministerChannelGroup"
required:
- subscriptions
encoding:
@@ -10443,6 +10445,8 @@ paths:
contentType: application/json
can_remove_subscribers_group:
contentType: application/json
can_administer_channel_group:
contentType: application/json
responses:
"200":
description: Success.
@@ -14906,6 +14910,7 @@ paths:
is_recently_active: {}
is_announcement_only: {}
can_remove_subscribers_group: {}
can_administer_channel_group: {}
stream_weekly_traffic:
type: integer
nullable: true
@@ -19736,6 +19741,7 @@ paths:
is_recently_active: {}
is_announcement_only: {}
can_remove_subscribers_group: {}
can_administer_channel_group: {}
stream_weekly_traffic:
type: integer
nullable: true
@@ -20086,6 +20092,47 @@ paths:
"new": {"direct_members": [10], "direct_subgroups": [11]},
"old": 15,
}
can_administer_channel_group:
description: |
The set of users who have permission to administer 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 administer the channel.
Realm admins are allowed to administer a channel they have access to
regardless of whether they are present in this group.
Users in this group can edit channel name and description without
subscribing to the channel, but they need to be subscribed to edit
channel permissions and add users.
**Changes**: New in Zulip 10.0 (feature level 325). Prior to this
change, the permission to administer channels was limited to realm
administrators.
type: object
additionalProperties: false
properties:
new:
allOf:
- description: |
The new [group-setting value](/api/group-setting-values) for who would
have the permission to administer this channel.
- $ref: "#/components/schemas/CanAdministerChannelGroup"
old:
allOf:
- description: |
The expected current [group-setting value](/api/group-setting-values)
for who has the permission to administer this channel.
- $ref: "#/components/schemas/CanAdministerChannelGroup"
required:
- new
example:
{
"new": {"direct_members": [10], "direct_subgroups": [11]},
"old": 15,
}
encoding:
is_private:
contentType: application/json
@@ -20101,6 +20148,8 @@ paths:
contentType: application/json
can_remove_subscribers_group:
contentType: application/json
can_administer_channel_group:
contentType: application/json
responses:
"200":
$ref: "#/components/responses/SimpleSuccess"
@@ -21815,6 +21864,7 @@ components:
is_recently_active: {}
is_announcement_only: {}
can_remove_subscribers_group: {}
can_administer_channel_group: {}
stream_weekly_traffic:
type: integer
nullable: true
@@ -21872,6 +21922,7 @@ components:
is_recently_active: {}
is_announcement_only: {}
can_remove_subscribers_group: {}
can_administer_channel_group: {}
required:
- stream_id
- name
@@ -22026,6 +22077,8 @@ components:
should use `stream_post_policy` instead.
can_remove_subscribers_group:
$ref: "#/components/schemas/CanRemoveSubscribersGroup"
can_administer_channel_group:
$ref: "#/components/schemas/CanAdministerChannelGroup"
BasicBot:
allOf:
- $ref: "#/components/schemas/BasicBotBase"
@@ -23055,6 +23108,8 @@ components:
insufficient data to estimate the average traffic.
can_remove_subscribers_group:
$ref: "#/components/schemas/CanRemoveSubscribersGroup"
can_administer_channel_group:
$ref: "#/components/schemas/CanAdministerChannelGroup"
is_archived:
type: boolean
description: |
@@ -24746,6 +24801,29 @@ components:
New in Zulip 6.0 (feature level 142).
[setting-values]: /api/group-setting-values
CanAdministerChannelGroup:
allOf:
- $ref: "#/components/schemas/GroupSettingValue"
- description: |
A [group-setting value][setting-values] defining the set of users
who have permission to administer this 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 administer the channel.
Realm admins are allowed to administer a channel they have access to
regardless of whether they are present in this group.
Users in this group can edit channel name and description without
subscribing to the channel, but they need to be subscribed to edit
channel permissions and add users.
**Changes**: New in Zulip 10.0 (feature level 325). Prior to this
change, the permission to administer channels was limited to realm
administrators.
[setting-values]: /api/group-setting-values
LinkifierPattern:
description: |

View File

@@ -4576,6 +4576,40 @@ class SubscribeActionTest(BaseAction):
),
)
moderators_group = NamedUserGroup.objects.get(
name=SystemGroups.MODERATORS,
is_system_group=True,
realm=self.user_profile.realm,
)
with self.verify_action(include_subscribers=include_subscribers, num_events=1) as events:
do_change_stream_group_based_setting(
stream,
"can_administer_channel_group",
moderators_group,
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_administer_channel_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)
stream.message_retention_days = 10

View File

@@ -543,6 +543,98 @@ class TestCreateStreams(ZulipTestCase):
# But it should be marked as read for Iago, the stream creator.
self.assert_length(iago_unread_messages, 0)
def test_can_administer_channel_group_on_stream_creation(self) -> None:
user = self.example_user("hamlet")
realm = user.realm
self.login_user(user)
moderators_system_group = NamedUserGroup.objects.get(
name="role:moderators", realm=realm, is_system_group=True
)
nobody_system_group = NamedUserGroup.objects.get(
name="role:nobody", realm=realm, is_system_group=True
)
subscriptions = [{"name": "new_stream1", "description": "First new stream"}]
result = self.common_subscribe_to_streams(
user,
subscriptions,
{"can_administer_channel_group": orjson.dumps(moderators_system_group.id).decode()},
subdomain="zulip",
)
self.assert_json_success(result)
stream = get_stream("new_stream1", realm)
self.assertEqual(stream.can_administer_channel_group.id, moderators_system_group.id)
subscriptions = [{"name": "new_stream2", "description": "Second new stream"}]
result = self.common_subscribe_to_streams(user, subscriptions, subdomain="zulip")
self.assert_json_success(result)
stream = get_stream("new_stream2", realm)
self.assertEqual(stream.can_administer_channel_group.id, nobody_system_group.id)
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
subscriptions = [{"name": "new_stream3", "description": "Third new stream"}]
result = self.common_subscribe_to_streams(
user,
subscriptions,
{"can_administer_channel_group": orjson.dumps(hamletcharacters_group.id).decode()},
allow_fail=True,
subdomain="zulip",
)
self.assert_json_success(result)
stream = get_stream("new_stream3", realm)
self.assertEqual(stream.can_administer_channel_group.id, hamletcharacters_group.id)
subscriptions = [{"name": "new_stream4", "description": "Fourth new stream"}]
result = self.common_subscribe_to_streams(
user,
subscriptions,
{
"can_administer_channel_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_administer_channel_group.direct_members.all()), [user])
self.assertEqual(
list(stream.can_administer_channel_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
)
result = self.common_subscribe_to_streams(
user,
subscriptions,
{"can_administer_channel_group": orjson.dumps(internet_group.id).decode()},
allow_fail=True,
subdomain="zulip",
)
self.assert_json_error(
result,
"'can_administer_channel_group' setting cannot be set to 'role:internet' group.",
)
everyone_group = NamedUserGroup.objects.get(
name="role:everyone", is_system_group=True, realm=realm
)
result = self.common_subscribe_to_streams(
user,
subscriptions,
{"can_administer_channel_group": orjson.dumps(everyone_group.id).decode()},
allow_fail=True,
subdomain="zulip",
)
self.assert_json_error(
result,
"'can_administer_channel_group' setting cannot be set to 'role:everyone' group.",
)
def test_can_remove_subscribers_group_on_stream_creation(self) -> None:
user = self.example_user("hamlet")
realm = user.realm
@@ -2443,6 +2535,100 @@ class StreamAdminTest(ZulipTestCase):
stream = get_stream("stream_name2", realm)
self.assertEqual(stream.can_remove_subscribers_group.id, moderators_system_group.id)
def test_change_stream_can_administer_channel_group(self) -> None:
user_profile = self.example_user("iago")
realm = user_profile.realm
stream = self.subscribe(user_profile, "stream_name1")
moderators_system_group = NamedUserGroup.objects.get(
name="role:moderators", realm=realm, is_system_group=True
)
self.login("shiva")
result = self.client_patch(
f"/json/streams/{stream.id}",
{
"can_administer_channel_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_administer_channel_group": orjson.dumps(
{"new": moderators_system_group.id}
).decode()
},
)
self.assert_json_success(result)
stream = get_stream("stream_name1", realm)
self.assertEqual(stream.can_administer_channel_group.id, moderators_system_group.id)
# This setting can be set to non-system groups.
hamletcharacters_group = NamedUserGroup.objects.get(name="hamletcharacters", realm=realm)
result = self.client_patch(
f"/json/streams/{stream.id}",
{
"can_administer_channel_group": orjson.dumps(
{"new": hamletcharacters_group.id}
).decode()
},
)
self.assert_json_success(result)
internet_group = NamedUserGroup.objects.get(
name="role:internet", is_system_group=True, realm=realm
)
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_administer_channel_group": orjson.dumps({"new": internet_group.id}).decode()},
)
self.assert_json_error(
result,
"'can_administer_channel_group' setting cannot be set to 'role:internet' group.",
)
everyone_group = NamedUserGroup.objects.get(
name="role:everyone", is_system_group=True, realm=realm
)
result = self.client_patch(
f"/json/streams/{stream.id}",
{"can_administer_channel_group": orjson.dumps({"new": everyone_group.id}).decode()},
)
self.assert_json_error(
result,
"'can_administer_channel_group' setting cannot be set to 'role:everyone' group.",
)
# For private streams, even admins must be subscribed to the stream to change
# can_administer_channel_group setting.
stream = self.make_stream("stream_name2", invite_only=True)
result = self.client_patch(
f"/json/streams/{stream.id}",
{
"can_administer_channel_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_administer_channel_group": orjson.dumps(
{"new": moderators_system_group.id}
).decode()
},
)
self.assert_json_success(result)
stream = get_stream("stream_name2", realm)
self.assertEqual(stream.can_administer_channel_group.id, moderators_system_group.id)
def test_stream_message_retention_days_on_stream_creation(self) -> None:
"""
Only admins can create streams with message_retention_days
@@ -2769,7 +2955,7 @@ class StreamAdminTest(ZulipTestCase):
are on.
"""
result = self.attempt_unsubscribe_of_principal(
query_count=17,
query_count=19,
target_users=[self.example_user("cordelia")],
is_realm_admin=True,
is_subbed=True,
@@ -2786,7 +2972,7 @@ class StreamAdminTest(ZulipTestCase):
streams you aren't on.
"""
result = self.attempt_unsubscribe_of_principal(
query_count=17,
query_count=19,
target_users=[self.example_user("cordelia")],
is_realm_admin=True,
is_subbed=False,
@@ -5140,7 +5326,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(17),
self.assert_database_query_count(18),
self.assert_memcached_count(3),
self.capture_send_event_calls(expected_num_events=7) as events,
):

View File

@@ -1487,6 +1487,9 @@ class UserGroupAPITestCase(UserGroupTestCase):
stream, setting_name, moderators_group, acting_user=None
)
# Unarchive the stream for the next test
do_unarchive_stream(stream, "support", acting_user=None)
leadership_group = self.create_user_group_for_test(
"leadership", acting_user=self.example_user("othello")
)

View File

@@ -259,6 +259,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_administer_channel_group: Json[GroupSettingChangeRequest] | None = None,
can_remove_subscribers_group: Json[GroupSettingChangeRequest] | None = None,
) -> HttpResponse:
# We allow realm administrators to update the stream name and
@@ -580,6 +581,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_administer_channel_group: Json[int | AnonymousSettingGroupDict] | None = None,
can_remove_subscribers_group: Json[int | AnonymousSettingGroupDict] | None = None,
announce: Json[bool] = False,
principals: Json[list[str] | list[int]] | None = None,
@@ -640,6 +642,9 @@ 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_administer_channel_group"] = group_settings_map[
"can_administer_channel_group"
]
stream_dict_copy["can_remove_subscribers_group"] = group_settings_map[
"can_remove_subscribers_group"
]