stream_settings: Add 'Default stream' option in create stream UI.

In this commit, we introduce a new option in the stream creation
UI - a 'Default stream for new users' checkbox. By default, the
checkbox is set to 'off' and is only visible to admins. This
allow admins to easily designate a stream as the default stream
for new users during stream creation.

Fixes #24048.
This commit is contained in:
Hemant Umre 2023-08-08 22:58:04 +05:30 committed by Tim Abbott
parent a81715786c
commit 63173ce1bc
10 changed files with 108 additions and 17 deletions

View File

@ -23,8 +23,11 @@ format used by the Zulip server that they are interacting with.
**Feature level 200** **Feature level 200**
* [`PATCH /streams/{stream_id}`](/api/update-stream): Added * [`PATCH /streams/{stream_id}`](/api/update-stream): Added
`is_default_stream` parameter to add or remove the stream as a default `is_default_stream` parameter to change whether the stream is a
stream for new users. default stream for new users in the organization.
* [`POST /users/me/subscriptions`](/api/subscribe): Added
`is_default_stream` parameter which determines whether any streams
created by this request will be default streams for new users.
**Feature level 199** **Feature level 199**

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# 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 = 199 API_FEATURE_LEVEL = 200
# 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

@ -217,6 +217,9 @@ function create_stream() {
data.invite_only = JSON.stringify(invite_only); data.invite_only = JSON.stringify(invite_only);
data.history_public_to_subscribers = JSON.stringify(history_public_to_subscribers); data.history_public_to_subscribers = JSON.stringify(history_public_to_subscribers);
const default_stream = $("#stream_creation_form .is_default_stream").prop("checked");
data.is_default_stream = JSON.stringify(default_stream);
const stream_post_policy = Number.parseInt( const stream_post_policy = Number.parseInt(
$("#stream_creation_form select[name=stream-post-policy]").val(), $("#stream_creation_form select[name=stream-post-policy]").val(),
10, 10,
@ -370,8 +373,10 @@ export function show_new_stream_modal() {
true, true,
); );
// set default state for "announce stream" option. // set default state for "announce stream" and "default stream" option.
$("#stream_creation_form .default-stream input").prop("checked", false);
update_announce_stream_state(); update_announce_stream_state();
stream_ui_updates.update_default_stream_and_stream_privacy_state($("#stream-creation"));
clear_error_display(); clear_error_display();
} }
@ -381,7 +386,14 @@ export function set_up_handlers() {
const $container = $("#stream-creation").expectOne(); const $container = $("#stream-creation").expectOne();
$container.on("change", ".stream-privacy-values input", update_announce_stream_state); $container.on("change", ".stream-privacy-values input", () => {
update_announce_stream_state();
stream_ui_updates.update_default_stream_and_stream_privacy_state($container);
});
$container.on("change", ".default-stream input", () => {
stream_ui_updates.update_default_stream_and_stream_privacy_state($container);
});
$container.on("click", ".finalize_create_stream", (e) => { $container.on("click", ".finalize_create_stream", (e) => {
e.preventDefault(); e.preventDefault();

View File

@ -717,6 +717,7 @@ export function setup_page(callback) {
stream_privacy_policy_values: stream_data.stream_privacy_policy_values, stream_privacy_policy_values: stream_data.stream_privacy_policy_values,
stream_privacy_policy, stream_privacy_policy,
stream_post_policy_values: stream_data.stream_post_policy_values, stream_post_policy_values: stream_data.stream_post_policy_values,
check_default_stream: false,
zulip_plan_is_not_limited: page_params.zulip_plan_is_not_limited, zulip_plan_is_not_limited: page_params.zulip_plan_is_not_limited,
org_level_message_retention_setting: org_level_message_retention_setting:
stream_edit.get_display_text_for_realm_message_retention_setting(), stream_edit.get_display_text_for_realm_message_retention_setting(),

View File

@ -115,6 +115,15 @@ export function update_regular_sub_settings(sub) {
export function update_default_stream_and_stream_privacy_state($container) { export function update_default_stream_and_stream_privacy_state($container) {
const $default_stream = $container.find(".default-stream"); const $default_stream = $container.find(".default-stream");
const is_stream_creation = $container.attr("id") === "stream-creation";
// In the stream creation UI, if the user is a non-admin hide the
// "Default stream for new users" widget
if (is_stream_creation && !page_params.is_admin) {
$default_stream.hide();
return;
}
const privacy_type = $container.find("input[type=radio][name=privacy]:checked").val(); const privacy_type = $container.find("input[type=radio][name=privacy]:checked").val();
const is_invite_only = const is_invite_only =
privacy_type === "invite-only" || privacy_type === "invite-only-public-history"; privacy_type === "invite-only" || privacy_type === "invite-only-public-history";

View File

@ -16,17 +16,15 @@
</div> </div>
</div> </div>
{{#if is_stream_edit}} <div class="default-stream">
<div class="default-stream"> {{> ../settings/settings_checkbox
{{> ../settings/settings_checkbox prefix="id_"
prefix="id_" setting_name="is_default_stream"
setting_name="is_default_stream" is_checked=check_default_stream
is_checked=check_default_stream label="Default stream for new users"
label="Default stream for new users" help_link="/help/set-default-streams-for-new-users"
help_link="/help/set-default-streams-for-new-users" }}
}} </div>
</div>
{{/if}}
<div class="input-group"> <div class="input-group">
<label class="dropdown-title">{{t 'Who can post to the stream?'}} <label class="dropdown-title">{{t 'Who can post to the stream?'}}

View File

@ -670,6 +670,7 @@ def list_to_streams(
user_profile: UserProfile, user_profile: UserProfile,
autocreate: bool = False, autocreate: bool = False,
unsubscribing_others: bool = False, unsubscribing_others: bool = False,
is_default_stream: bool = False,
) -> Tuple[List[Stream], List[Stream]]: ) -> Tuple[List[Stream], List[Stream]]:
"""Converts list of dicts to a list of Streams, validating input in the process """Converts list of dicts to a list of Streams, validating input in the process
@ -736,6 +737,10 @@ def list_to_streams(
raise JsonableError(_("Insufficient permission")) raise JsonableError(_("Insufficient permission"))
if not invite_only and not user_profile.can_create_public_streams(): if not invite_only and not user_profile.can_create_public_streams():
raise JsonableError(_("Insufficient permission")) raise JsonableError(_("Insufficient permission"))
if is_default_stream and not user_profile.is_realm_admin:
raise JsonableError(_("Insufficient permission"))
if invite_only and is_default_stream:
raise JsonableError(_("A default stream cannot be private."))
if not autocreate: if not autocreate:
raise JsonableError( raise JsonableError(

View File

@ -8657,6 +8657,21 @@ paths:
type: boolean type: boolean
default: false default: false
example: true example: true
- name: is_default_stream
in: query
description: |
This parameter determines whether any newly created streams will be
added as [default streams][default-streams] for new users joining
the organization.
[default-streams]: /help/set-default-streams-for-new-users
**Changes**: New in Zulip 8.0 (feature level 200). Previously, default stream status
could only be changed using the [dedicated API endpoint](/api/add-default-stream).
schema:
type: boolean
default: false
example: true
- $ref: "#/components/parameters/HistoryPublicToSubscribers" - $ref: "#/components/parameters/HistoryPublicToSubscribers"
- $ref: "#/components/parameters/StreamPostPolicy" - $ref: "#/components/parameters/StreamPostPolicy"
- $ref: "#/components/parameters/MessageRetentionDays" - $ref: "#/components/parameters/MessageRetentionDays"

View File

@ -383,6 +383,47 @@ class TestCreateStreams(ZulipTestCase):
if stream.name == "publictrywithouthistory": if stream.name == "publictrywithouthistory":
self.assertTrue(stream.history_public_to_subscribers) self.assertTrue(stream.history_public_to_subscribers)
def test_add_stream_as_default_on_stream_creation(self) -> None:
user_profile = self.example_user("hamlet")
self.login_user(user_profile)
realm = user_profile.realm
post_data = {
"subscriptions": orjson.dumps(
[{"name": "default_stream", "description": "This stream is default for new users"}]
).decode(),
"is_default_stream": orjson.dumps(True).decode(),
}
result = self.api_post(
user_profile, "/api/v1/users/me/subscriptions", post_data, subdomain="zulip"
)
self.assert_json_error(result, "Insufficient permission")
do_change_user_role(user_profile, UserProfile.ROLE_REALM_ADMINISTRATOR, acting_user=None)
result = self.api_post(
user_profile, "/api/v1/users/me/subscriptions", post_data, subdomain="zulip"
)
self.assert_json_success(result)
default_stream = get_stream("default_stream", realm)
self.assertTrue(default_stream.id in get_default_stream_ids_for_realm(realm.id))
post_data = {
"subscriptions": orjson.dumps(
[
{
"name": "private_default_stream",
"description": "This stream is private and default for new users",
}
]
).decode(),
"invite_only": orjson.dumps(True).decode(),
"is_default_stream": orjson.dumps(True).decode(),
}
result = self.api_post(
user_profile, "/api/v1/users/me/subscriptions", post_data, subdomain="zulip"
)
self.assert_json_error(result, "A default stream cannot be private.")
def test_history_public_to_subscribers_zephyr_realm(self) -> None: def test_history_public_to_subscribers_zephyr_realm(self) -> None:
realm = get_realm("zephyr") realm = get_realm("zephyr")

View File

@ -578,6 +578,7 @@ def add_subscriptions_backend(
), ),
invite_only: bool = REQ(json_validator=check_bool, default=False), invite_only: bool = REQ(json_validator=check_bool, default=False),
is_web_public: bool = REQ(json_validator=check_bool, default=False), is_web_public: bool = REQ(json_validator=check_bool, default=False),
is_default_stream: bool = REQ(json_validator=check_bool, default=False),
stream_post_policy: int = REQ( stream_post_policy: int = REQ(
json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES), json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES),
default=Stream.STREAM_POST_POLICY_EVERYONE, default=Stream.STREAM_POST_POLICY_EVERYONE,
@ -660,7 +661,9 @@ def add_subscriptions_backend(
# Validation of the streams arguments, including enforcement of # Validation of the streams arguments, including enforcement of
# can_create_streams policy and check_stream_name policy is inside # can_create_streams policy and check_stream_name policy is inside
# list_to_streams. # list_to_streams.
existing_streams, created_streams = list_to_streams(stream_dicts, user_profile, autocreate=True) existing_streams, created_streams = list_to_streams(
stream_dicts, user_profile, autocreate=True, is_default_stream=is_default_stream
)
authorized_streams, unauthorized_streams = filter_stream_authorization( authorized_streams, unauthorized_streams = filter_stream_authorization(
user_profile, existing_streams user_profile, existing_streams
) )
@ -682,6 +685,10 @@ def add_subscriptions_backend(
_("You can only invite other Zephyr mirroring users to private streams.") _("You can only invite other Zephyr mirroring users to private streams.")
) )
if is_default_stream:
for stream in created_streams:
do_add_default_stream(stream)
(subscribed, already_subscribed) = bulk_add_subscriptions( (subscribed, already_subscribed) = bulk_add_subscriptions(
realm, streams, subscribers, acting_user=user_profile, color_map=color_map realm, streams, subscribers, acting_user=user_profile, color_map=color_map
) )