mirror of
https://github.com/zulip/zulip.git
synced 2025-10-28 10:33:54 +00:00
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:
committed by
Tim Abbott
parent
44b498f96b
commit
eb943d54a9
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
]
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
):
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user