stream: Add do_change_can_remove_subscribers_group and field to objects.

This commit adds do_change_can_remove_subscriber_group function for
changing can_remove_subscribers_group field of a stream. We also add
can_remove_subscribers_group_id field to stream and subscription
objects.

This function will be helpful for writing tests in next commit.
We would add API and UI support to change this setting in further
commits.
This commit is contained in:
Sahil Batra 2022-06-27 22:09:33 +05:30 committed by Tim Abbott
parent 86c2f6881e
commit b9248c75f4
10 changed files with 102 additions and 1 deletions

View File

@ -50,6 +50,7 @@ exports.test_streams = {
is_web_public: false, is_web_public: false,
message_retention_days: null, message_retention_days: null,
stream_post_policy: 1, stream_post_policy: 1,
can_remove_subscribers_group_id: 2,
}, },
test: { test: {
name: "test", name: "test",
@ -64,6 +65,7 @@ exports.test_streams = {
is_announcement_only: false, is_announcement_only: false,
message_retention_days: null, message_retention_days: null,
stream_post_policy: 1, stream_post_policy: 1,
can_remove_subscribers_group_id: 2,
}, },
}; };

View File

@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 6.0 ## Changes in Zulip 6.0
**Feature level 142**
* [`GET users/me/subscriptions`](/api/get-subscriptions), [`GET
/streams`](/api/get-streams), [`POST /register`](/api/register-queue),
[`GET /events`](/api/get-events): Added `can_remove_subscribers_group_id`
field to Stream and Subscription objects.
**Feature level 141** **Feature level 141**
* [`POST /register`](/api/register-queue), [`PATCH * [`POST /register`](/api/register-queue), [`PATCH

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md, as well as # new level means in templates/zerver/api/changelog.md, as well as
# "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 141 API_FEATURE_LEVEL = 142
# 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

@ -68,6 +68,7 @@ from zerver.models import (
Recipient, Recipient,
Stream, Stream,
Subscription, Subscription,
UserGroup,
UserProfile, UserProfile,
active_non_guest_user_ids, active_non_guest_user_ids,
get_system_bot, get_system_bot,
@ -289,6 +290,7 @@ def send_subscription_add_events(
stream_weekly_traffic=stream_info.stream_weekly_traffic, stream_weekly_traffic=stream_info.stream_weekly_traffic,
subscribers=stream_info.subscribers, subscribers=stream_info.subscribers,
# Fields from Stream.API_FIELDS # Fields from Stream.API_FIELDS
can_remove_subscribers_group_id=stream_dict["can_remove_subscribers_group_id"],
date_created=stream_dict["date_created"], date_created=stream_dict["date_created"],
description=stream_dict["description"], description=stream_dict["description"],
first_message_id=stream_dict["first_message_id"], first_message_id=stream_dict["first_message_id"],
@ -1317,3 +1319,39 @@ def do_change_stream_message_retention_days(
old_value=old_message_retention_days_value, old_value=old_message_retention_days_value,
new_value=message_retention_days, new_value=message_retention_days,
) )
def do_change_can_remove_subscribers_group(
stream: Stream, user_group: UserGroup, *, acting_user: Optional[UserProfile] = None
) -> None:
old_user_group = stream.can_remove_subscribers_group
old_user_group_id = None
if old_user_group is not None:
old_user_group_id = old_user_group.id
stream.can_remove_subscribers_group = user_group
stream.save()
RealmAuditLog.objects.create(
realm=stream.realm,
acting_user=acting_user,
modified_stream=stream,
event_type=RealmAuditLog.STREAM_CAN_REMOVE_SUBSCRIBERS_GROUP_CHANGED,
event_time=timezone_now(),
extra_data=orjson.dumps(
{
RealmAuditLog.OLD_VALUE: old_user_group_id,
RealmAuditLog.NEW_VALUE: user_group.id,
}
).decode(),
)
event = dict(
op="update",
type="stream",
property="can_remove_subscribers_group_id",
value=user_group.id,
stream_id=stream.id,
name=stream.name,
)
transaction.on_commit(
lambda: send_event(stream.realm, event, can_access_stream_user_ids(stream))
)

View File

@ -50,6 +50,7 @@ from zerver.models import Realm, RealmUserDefault, Stream, UserProfile
# 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.
basic_stream_fields = [ basic_stream_fields = [
("can_remove_subscribers_group_id", int),
("date_created", int), ("date_created", int),
("description", str), ("description", str),
("first_message_id", OptionalType(int)), ("first_message_id", OptionalType(int)),
@ -1287,6 +1288,9 @@ def check_stream_update(
elif prop == "stream_post_policy": elif prop == "stream_post_policy":
assert extra_keys == set() assert extra_keys == set()
assert value in Stream.STREAM_POST_POLICY_TYPES assert value in Stream.STREAM_POST_POLICY_TYPES
elif prop == "can_remove_subscribers_group_id":
assert extra_keys == set()
assert isinstance(value, int)
else: else:
raise AssertionError(f"Unknown property: {prop}") raise AssertionError(f"Unknown property: {prop}")

View File

@ -40,6 +40,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo:
subscribed = [] subscribed = []
for stream in get_web_public_streams_queryset(realm): for stream in get_web_public_streams_queryset(realm):
# Add Stream fields. # Add Stream fields.
can_remove_subscribers_group_id = stream.can_remove_subscribers_group_id
date_created = datetime_to_timestamp(stream.date_created) date_created = datetime_to_timestamp(stream.date_created)
description = stream.description description = stream.description
first_message_id = stream.first_message_id first_message_id = stream.first_message_id
@ -71,6 +72,7 @@ def get_web_public_subs(realm: Realm) -> SubscriptionInfo:
sub = SubscriptionStreamDict( sub = SubscriptionStreamDict(
audible_notifications=audible_notifications, audible_notifications=audible_notifications,
can_remove_subscribers_group_id=can_remove_subscribers_group_id,
color=color, color=color,
date_created=date_created, date_created=date_created,
description=description, description=description,
@ -110,6 +112,7 @@ def build_stream_dict_for_sub(
recent_traffic: Dict[int, int], recent_traffic: Dict[int, int],
) -> SubscriptionStreamDict: ) -> SubscriptionStreamDict:
# Handle Stream.API_FIELDS # Handle Stream.API_FIELDS
can_remove_subscribers_group_id = raw_stream_dict["can_remove_subscribers_group_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"]
first_message_id = raw_stream_dict["first_message_id"] first_message_id = raw_stream_dict["first_message_id"]
@ -153,6 +156,7 @@ def build_stream_dict_for_sub(
# Our caller may add a subscribers field. # Our caller may add a subscribers field.
return SubscriptionStreamDict( return SubscriptionStreamDict(
audible_notifications=audible_notifications, audible_notifications=audible_notifications,
can_remove_subscribers_group_id=can_remove_subscribers_group_id,
color=color, color=color,
date_created=date_created, date_created=date_created,
description=description, description=description,
@ -182,6 +186,7 @@ def build_stream_dict_for_never_sub(
raw_stream_dict: RawStreamDict, raw_stream_dict: RawStreamDict,
recent_traffic: Dict[int, int], recent_traffic: Dict[int, int],
) -> NeverSubscribedStreamDict: ) -> NeverSubscribedStreamDict:
can_remove_subscribers_group_id = raw_stream_dict["can_remove_subscribers_group_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"]
first_message_id = raw_stream_dict["first_message_id"] first_message_id = raw_stream_dict["first_message_id"]
@ -202,6 +207,7 @@ def build_stream_dict_for_never_sub(
# Our caller may add a subscribers field. # Our caller may add a subscribers field.
return NeverSubscribedStreamDict( return NeverSubscribedStreamDict(
can_remove_subscribers_group_id=can_remove_subscribers_group_id,
date_created=date_created, date_created=date_created,
description=description, description=description,
first_message_id=first_message_id, first_message_id=first_message_id,

View File

@ -177,6 +177,7 @@ class RawStreamDict(TypedDict):
are needed to encode the stream for the API. are needed to encode the stream for the API.
""" """
can_remove_subscribers_group_id: int
date_created: datetime.datetime date_created: datetime.datetime
description: str description: str
email_token: str email_token: str
@ -216,6 +217,7 @@ class SubscriptionStreamDict(TypedDict):
""" """
audible_notifications: Optional[bool] audible_notifications: Optional[bool]
can_remove_subscribers_group_id: int
color: str color: str
date_created: int date_created: int
description: str description: str
@ -242,6 +244,7 @@ class SubscriptionStreamDict(TypedDict):
class NeverSubscribedStreamDict(TypedDict): class NeverSubscribedStreamDict(TypedDict):
can_remove_subscribers_group_id: int
date_created: int date_created: int
description: str description: str
first_message_id: Optional[int] first_message_id: Optional[int]
@ -264,6 +267,7 @@ class APIStreamDict(TypedDict):
with few exceptions and possible additional fields. with few exceptions and possible additional fields.
""" """
can_remove_subscribers_group_id: int
date_created: int date_created: int
description: str description: str
first_message_id: Optional[int] first_message_id: Optional[int]

View File

@ -2552,6 +2552,7 @@ class Stream(models.Model):
"name", "name",
"rendered_description", "rendered_description",
"stream_post_policy", "stream_post_policy",
"can_remove_subscribers_group_id",
] ]
@staticmethod @staticmethod
@ -2561,6 +2562,7 @@ class Stream(models.Model):
def to_dict(self) -> APIStreamDict: def to_dict(self) -> APIStreamDict:
return APIStreamDict( return APIStreamDict(
can_remove_subscribers_group_id=self.can_remove_subscribers_group_id,
date_created=datetime_to_timestamp(self.date_created), date_created=datetime_to_timestamp(self.date_created),
description=self.description, description=self.description,
first_message_id=self.first_message_id, first_message_id=self.first_message_id,
@ -4364,6 +4366,7 @@ class AbstractRealmAuditLog(models.Model):
STREAM_REACTIVATED = 604 STREAM_REACTIVATED = 604
STREAM_MESSAGE_RETENTION_DAYS_CHANGED = 605 STREAM_MESSAGE_RETENTION_DAYS_CHANGED = 605
STREAM_PROPERTY_CHANGED = 607 STREAM_PROPERTY_CHANGED = 607
STREAM_CAN_REMOVE_SUBSCRIBERS_GROUP_CHANGED = 608
# The following values are only for RemoteZulipServerAuditLog # The following values are only for RemoteZulipServerAuditLog
# Values should be exactly 10000 greater than the corresponding # Values should be exactly 10000 greater than the corresponding

View File

@ -644,6 +644,7 @@ paths:
"in_home_view": true, "in_home_view": true,
"email_address": "test_stream.af64447e9e39374841063747ade8e6b0.show-sender@testserver", "email_address": "test_stream.af64447e9e39374841063747ade8e6b0.show-sender@testserver",
"stream_weekly_traffic": null, "stream_weekly_traffic": null,
"can_remove_subscribers_group_id": 2,
"subscribers": [10], "subscribers": [10],
}, },
], ],
@ -1143,6 +1144,7 @@ paths:
"first_message_id": null, "first_message_id": null,
"message_retention_days": null, "message_retention_days": null,
"is_announcement_only": false, "is_announcement_only": false,
"can_remove_subscribers_group_id": 2,
}, },
], ],
"id": 0, "id": 0,
@ -1188,6 +1190,7 @@ paths:
"first_message_id": null, "first_message_id": null,
"message_retention_days": null, "message_retention_days": null,
"is_announcement_only": false, "is_announcement_only": false,
"can_remove_subscribers_group_id": 2,
}, },
], ],
"id": 0, "id": 0,
@ -1745,6 +1748,7 @@ paths:
"first_message_id": 1, "first_message_id": 1,
"message_retention_days": null, "message_retention_days": null,
"is_announcement_only": false, "is_announcement_only": false,
"can_remove_subscribers_group_id": 2,
}, },
{ {
"name": "Denmark", "name": "Denmark",
@ -1758,6 +1762,7 @@ paths:
"first_message_id": 4, "first_message_id": 4,
"message_retention_days": null, "message_retention_days": null,
"is_announcement_only": false, "is_announcement_only": false,
"can_remove_subscribers_group_id": 2,
}, },
{ {
"name": "Verona", "name": "Verona",
@ -1771,6 +1776,7 @@ paths:
"first_message_id": 6, "first_message_id": 6,
"message_retention_days": null, "message_retention_days": null,
"is_announcement_only": false, "is_announcement_only": false,
"can_remove_subscribers_group_id": 2,
}, },
], ],
}, },
@ -1815,6 +1821,7 @@ paths:
"first_message_id": 1, "first_message_id": 1,
"message_retention_days": null, "message_retention_days": null,
"is_announcement_only": false, "is_announcement_only": false,
"can_remove_subscribers_group_id": 2,
}, },
], ],
"id": 0, "id": 0,
@ -10089,6 +10096,7 @@ paths:
first_message_id: first_message_id:
nullable: true nullable: true
is_announcement_only: {} is_announcement_only: {}
can_remove_subscribers_group_id: {}
stream_weekly_traffic: stream_weekly_traffic:
type: integer type: integer
nullable: true nullable: true
@ -13879,6 +13887,7 @@ paths:
first_message_id: first_message_id:
nullable: true nullable: true
is_announcement_only: {} is_announcement_only: {}
can_remove_subscribers_group_id: {}
is_default: is_default:
type: boolean type: boolean
description: | description: |
@ -13989,6 +13998,7 @@ paths:
"rendered_description": "<p>A Scandinavian country</p>", "rendered_description": "<p>A Scandinavian country</p>",
"stream_id": 7, "stream_id": 7,
"stream_post_policy": 1, "stream_post_policy": 1,
"can_remove_subscribers_group_id": 2,
}, },
} }
"400": "400":
@ -15075,6 +15085,7 @@ components:
first_message_id: first_message_id:
nullable: true nullable: true
is_announcement_only: {} is_announcement_only: {}
can_remove_subscribers_group_id: {}
BasicStreamBase: BasicStreamBase:
type: object type: object
description: | description: |
@ -15176,6 +15187,13 @@ 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_id:
type: integer
description: |
ID of the user group whose members are allowed to unsubscribe others
from the stream.
**Changes**: New in Zulip 6.0 (feature level 142).
BasicBot: BasicBot:
allOf: allOf:
- $ref: "#/components/schemas/BasicBotBase" - $ref: "#/components/schemas/BasicBotBase"
@ -15792,6 +15810,13 @@ components:
Null means the stream was recently created and there is Null means the stream was recently created and there is
insufficient data to estimate the average traffic. insufficient data to estimate the average traffic.
can_remove_subscribers_group_id:
type: integer
description: |
ID of the user group whose members are allowed to unsubscribe others
from the stream.
**Changes**: New in Zulip 6.0 (feature level 142).
DefaultStreamGroup: DefaultStreamGroup:
type: object type: object
description: | description: |

View File

@ -79,6 +79,7 @@ from zerver.actions.realm_settings import (
from zerver.actions.streams import ( from zerver.actions.streams import (
bulk_add_subscriptions, bulk_add_subscriptions,
bulk_remove_subscriptions, bulk_remove_subscriptions,
do_change_can_remove_subscribers_group,
do_change_stream_description, do_change_stream_description,
do_change_stream_message_retention_days, do_change_stream_message_retention_days,
do_change_stream_permission, do_change_stream_permission,
@ -2885,6 +2886,17 @@ class SubscribeActionTest(BaseAction):
events = self.verify_action(action, include_subscribers=include_subscribers, num_events=2) events = self.verify_action(action, include_subscribers=include_subscribers, num_events=2)
check_stream_update("events[0]", events[0]) check_stream_update("events[0]", events[0])
moderators_group = UserGroup.objects.get(
name=UserGroup.MODERATORS_GROUP_NAME,
is_system_group=True,
realm=self.user_profile.realm,
)
action = lambda: do_change_can_remove_subscribers_group(
stream, moderators_group, acting_user=self.example_user("hamlet")
)
events = self.verify_action(action, include_subscribers=include_subscribers, num_events=1)
check_stream_update("events[0]", events[0])
# 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)
stream.message_retention_days = 10 stream.message_retention_days = 10