settings: Migrate to stream_post_policy structure.

This commit includes a new `stream_post_policy` setting,
by replacing the `is_announcement_only` field from the Stream model,
which is done by mirroring the structure of the existing
`create_stream_policy`.

It includes the necessary schema and database migrations to migrate
the is_announcement_only boolean field to stream_post_policy,
a smallPositiveInteger field similar to many other settings.

This change is done to allow organization administrators to restrict
new members from creating and posting to a stream. However, this does
not affect admins who are new members.

With many tweaks by tabbott to documentation under /help, etc.

Fixes #13616.
This commit is contained in:
Ryan Rehman 2020-02-05 02:20:55 +05:30 committed by Tim Abbott
parent 31aecc0abb
commit 174b2abcfd
60 changed files with 525 additions and 189 deletions

View File

@ -81,6 +81,13 @@ people.small_avatar_url_for_person = function () {
return 'http://example.com/example.png';
};
const new_user = {
email: 'new_user@example.com',
user_id: 101,
full_name: 'New User',
date_joined: new Date(),
};
const me = {
email: 'me@example.com',
user_id: 30,
@ -100,6 +107,7 @@ const bob = {
full_name: 'Bob',
};
people.add(new_user);
people.add(me);
people.initialize_current_user(me.user_id);
@ -315,30 +323,30 @@ run_test('validate_stream_message', () => {
assert($("#compose-all-everyone").visible());
});
run_test('test_validate_stream_message_announcement_only', () => {
run_test('test_validate_stream_message_post_policy', () => {
// This test is in continuation with test_validate but it has been seperated out
// for better readabilty. Their relative position of execution should not be changed.
// Although the position with respect to test_validate_stream_message does not matter
// as `get_announcement_only` is reset at the end.
// as `get_stream_post_policy` is reset at the end.
page_params.is_admin = false;
const sub = {
stream_id: 102,
name: 'stream102',
subscribed: true,
announcement_only: true,
stream_post_policy: stream_data.stream_post_policy_values.admins.code,
};
stream_data.get_announcement_only = function () {
return true;
stream_data.get_stream_post_policy = function () {
return 2;
};
compose_state.topic('subject102');
stream_data.add_sub('stream102', sub);
assert(!compose.validate());
assert.equal($('#compose-error-msg').html(), i18n.t("Only organization admins are allowed to post to this stream."));
// reset `get_announcement_only` so that any tests occurung after this
// reset `get_stream_post_policy` so that any tests occurung after this
// do not reproduce this error.
stream_data.get_announcement_only = function () {
return false;
stream_data.get_stream_post_policy = function () {
return stream_data.stream_post_policy_values.everyone.code;
};
});

View File

@ -13,6 +13,8 @@ set_global('$', global.make_zjquery());
set_global('compose_pm_pill', {
});
set_global('i18n', global.stub_i18n);
zrequire('people');
zrequire('compose_ui');
zrequire('compose');

View File

@ -1,4 +1,5 @@
set_global('blueslip', {});
set_global('i18n', global.stub_i18n);
global.blueslip.warn = function () {};
zrequire('util');

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('util');
zrequire('unread');
zrequire('stream_data');

View File

@ -59,6 +59,7 @@ const denmark_stream = {
// prefer to test with a clean slate.
set_global('page_params', {});
set_global('i18n', global.stub_i18n);
zrequire('stream_data');
set_global('i18n', global.stub_i18n);

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('hash_util');
zrequire('stream_data');
zrequire('people');

View File

@ -5,6 +5,8 @@ set_global('location', {
host: 'example.com',
});
set_global('to_$', () => window_stub);
set_global('i18n', global.stub_i18n);
zrequire('people');
zrequire('hash_util');
zrequire('hashchange');

View File

@ -1,6 +1,8 @@
zrequire('hash_util');
set_global('katex', zrequire('katex', 'katex/dist/katex.min.js'));
set_global('marked', zrequire('marked', 'third/marked/lib/marked'));
set_global('i18n', global.stub_i18n);
zrequire('util');
zrequire('fenced_code');
zrequire('stream_data');

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('muting');
zrequire('stream_data');
set_global('blueslip', global.make_zblueslip());

View File

@ -1,3 +1,4 @@
set_global('i18n', global.stub_i18n);
set_global('$', global.make_zjquery());
zrequire('hash_util');
zrequire('hashchange');

View File

@ -1,3 +1,4 @@
set_global('i18n', global.stub_i18n);
set_global('$', global.make_zjquery());
zrequire('narrow_state');

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('Filter', 'js/filter');
zrequire('MessageListData', 'js/message_list_data');
zrequire('narrow_state');

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('people');
zrequire('Filter', 'js/filter');
zrequire('stream_data');

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('Filter', 'js/filter');
zrequire('people');
zrequire('stream_data');

View File

@ -20,6 +20,8 @@ const _navigator = {
};
set_global('navigator', _navigator);
set_global('i18n', global.stub_i18n);
zrequire('alert_words');
zrequire('muting');
zrequire('stream_data');

View File

@ -6,6 +6,8 @@ set_global('message_store', {
user_ids: () => [],
});
set_global('i18n', global.stub_i18n);
zrequire('util');
zrequire('typeahead_helper');
set_global('Handlebars', global.make_handlebars());

View File

@ -5,6 +5,8 @@ set_global('message_store', {
user_ids: () => [],
});
set_global('i18n', global.stub_i18n);
zrequire('util');
zrequire('typeahead_helper');
set_global('Handlebars', global.make_handlebars());

View File

@ -1,3 +1,4 @@
set_global('i18n', global.stub_i18n);
set_global('$', global.make_zjquery());
zrequire('settings_muting');

View File

@ -58,7 +58,7 @@ run_test('basics', () => {
stream_id: 2,
is_muted: false,
invite_only: true,
is_announcement_only: true,
stream_post_policy: stream_data.stream_post_policy_values.admins.code,
};
const test = {
subscribed: true,
@ -88,8 +88,8 @@ run_test('basics', () => {
assert(stream_data.get_invite_only('social'));
assert(!stream_data.get_invite_only('unknown'));
assert(stream_data.get_announcement_only('social'));
assert(!stream_data.get_announcement_only('unknown'));
assert(stream_data.get_stream_post_policy('social'));
assert(!stream_data.get_stream_post_policy('unknown'));
assert.equal(stream_data.get_color('social'), 'red');
assert.equal(stream_data.get_color('unknown'), global.stream_color.default_color);
@ -458,7 +458,7 @@ run_test('stream_settings', () => {
subscribed: true,
invite_only: true,
history_public_to_subscribers: true,
is_announcement_only: true,
stream_post_policy: stream_data.stream_post_policy_values.admins.code,
};
stream_data.clear_subscriptions();
stream_data.add_sub(cinnamon.name, cinnamon);
@ -479,18 +479,20 @@ run_test('stream_settings', () => {
assert.equal(sub_rows[2].invite_only, false);
assert.equal(sub_rows[0].history_public_to_subscribers, true);
assert.equal(sub_rows[0].is_announcement_only, true);
assert.equal(sub_rows[0].stream_post_policy ===
stream_data.stream_post_policy_values.admins.code, true);
const sub = stream_data.get_sub('a');
stream_data.update_stream_privacy(sub, {
invite_only: false,
history_public_to_subscribers: false,
});
stream_data.update_stream_announcement_only(sub, false);
stream_data.update_stream_post_policy(sub, 1);
stream_data.update_calculated_fields(sub);
assert.equal(sub.invite_only, false);
assert.equal(sub.history_public_to_subscribers, false);
assert.equal(sub.is_announcement_only, false);
assert.equal(sub.stream_post_policy,
stream_data.stream_post_policy_values.everyone.code);
// For guest user only retrieve subscribed streams
sub_rows = stream_data.get_updated_unsorted_subs();

View File

@ -2,6 +2,7 @@ const noop = function () {};
const return_true = function () { return true; };
set_global('$', global.make_zjquery());
set_global('document', 'document-stub');
set_global('i18n', global.stub_i18n);
set_global('colorspace', {
sRGB_to_linear: noop,
@ -150,14 +151,14 @@ run_test('update_property', () => {
});
});
// Test stream is_announcement_only change event
// Test stream stream_post_policy change event
with_overrides(function (override) {
global.with_stub(function (stub) {
override('subs.update_stream_announcement_only', stub.f);
stream_events.update_property(1, 'is_announcement_only', true);
override('subs.update_stream_post_policy', stub.f);
stream_events.update_property(1, 'stream_post_policy', stream_data.stream_post_policy_values.admins.code);
const args = stub.get_args('sub', 'val');
assert.equal(args.sub.stream_id, 1);
assert.equal(args.val, true);
assert.equal(args.val, stream_data.stream_post_policy_values.admins.code);
});
});
});

View File

@ -1,6 +1,7 @@
set_global('document', 'document-stub');
set_global('$', global.make_zjquery());
set_global('blueslip', global.make_zblueslip());
set_global('i18n', global.stub_i18n);
const FoldDict = zrequire('fold_dict').FoldDict;
const IntDict = zrequire('int_dict').IntDict;

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('util');
zrequire('stream_data');
zrequire('stream_sort');

View File

@ -1,3 +1,4 @@
set_global('i18n', global.stub_i18n);
global.stub_out_jquery();
set_global('ui', {

View File

@ -5,6 +5,7 @@ set_global('i18n', global.stub_i18n);
set_global('page_params', {});
zrequire('settings_notifications');
zrequire('stream_edit');
zrequire('stream_data');
const { JSDOM } = require("jsdom");
const { window } = new JSDOM();
@ -1281,6 +1282,7 @@ run_test('subscription_stream_privacy_modal', () => {
stream_id: 999,
is_private: true,
is_admin: true,
stream_post_policy_values: stream_data.stream_post_policy_values,
};
const html = render('subscription_stream_privacy_modal', args);
@ -1289,8 +1291,13 @@ run_test('subscription_stream_privacy_modal', () => {
assert.equal(other_options[1].value, 'invite-only-public-history');
assert.equal(other_options[2].value, 'invite-only');
const is_announcement_only = $(html).find("input[name=is-announcement-only]");
assert.equal(is_announcement_only.prop('checked'), false);
const stream_post_policy = $(html).find("input[name=stream-post-policy]");
assert.equal(stream_post_policy[0].value,
stream_data.stream_post_policy_values.everyone.code);
assert.equal(stream_post_policy[1].value,
stream_data.stream_post_policy_values.admins.code);
assert.equal(stream_post_policy[2].value,
stream_data.stream_post_policy_values.non_new_members.code);
const button = $(html).find("#change-stream-privacy-button");
assert(button.hasClass("btn-danger"));

View File

@ -1,3 +1,5 @@
set_global('i18n', global.stub_i18n);
zrequire('unread');
zrequire('util');
zrequire('stream_data');

View File

@ -2,6 +2,7 @@ set_global('blueslip', global.make_zblueslip());
set_global('pm_conversations', {
recent: {},
});
set_global('i18n', global.stub_i18n);
zrequire('muting');
zrequire('unread');

View File

@ -2,6 +2,7 @@ set_global('narrow_state', {});
set_global('unread', {});
set_global('muting', {});
set_global('message_list', {});
set_global('i18n', global.stub_i18n);
zrequire('hash_util');
zrequire('stream_data');

View File

@ -477,13 +477,30 @@ function validate_stream_message_announce(stream_name) {
return true;
}
function validate_stream_message_announcement_only(stream_name) {
// Only allow realm admins to post to announcement_only streams.
const is_announcement_only = stream_data.get_announcement_only(stream_name);
if (is_announcement_only && !page_params.is_admin) {
function validate_stream_message_post_policy(stream_name) {
if (page_params.is_admin) {
return true;
}
const stream_post_permission_type = stream_data.stream_post_policy_values;
const stream_post_policy = stream_data.get_stream_post_policy(stream_name);
if (stream_post_policy === stream_post_permission_type.admins.code) {
compose_error(i18n.t("Only organization admins are allowed to post to this stream."));
return false;
}
const person = people.get_person_from_user_id(page_params.user_id);
const current_datetime = new Date(Date.now());
const person_date_joined = new Date(person.date_joined);
const days = new Date(current_datetime - person_date_joined).getDate();
let error_text;
if (stream_post_policy === stream_post_permission_type.non_new_members.code &&
days < page_params.realm_waiting_period_threshold) {
error_text = i18n.t("New members are not allowed to post to this stream.<br>Permission will be granted in __days__ days.", {days: days});
compose_error(error_text);
return false;
}
return true;
}
@ -537,7 +554,7 @@ function validate_stream_message() {
}
}
if (!validate_stream_message_announcement_only(stream_name)) {
if (!validate_stream_message_post_policy(stream_name)) {
return false;
}

View File

@ -98,7 +98,7 @@ const stream_name_error = (function () {
}());
function ajaxSubscribeForCreation(stream_name, description, user_ids, invite_only,
is_announcement_only, announce, history_public_to_subscribers) {
stream_post_policy, announce, history_public_to_subscribers) {
// TODO: We can eliminate the user_ids -> principals conversion
// once we upgrade the backend to accept user_ids.
const persons = _.compact(_.map(user_ids, (user_id) => {
@ -114,7 +114,7 @@ function ajaxSubscribeForCreation(stream_name, description, user_ids, invite_onl
description: description}]),
principals: JSON.stringify(principals),
invite_only: JSON.stringify(invite_only),
is_announcement_only: JSON.stringify(is_announcement_only),
stream_post_policy: JSON.stringify(stream_post_policy),
announce: JSON.stringify(announce),
history_public_to_subscribers: JSON.stringify(history_public_to_subscribers),
},
@ -182,12 +182,18 @@ function create_stream() {
const stream_name = $.trim($("#create_stream_name").val());
const description = $.trim($("#create_stream_description").val());
const privacy_setting = $('#stream_creation_form input[name=privacy]:checked').val();
const is_announcement_only = $('#stream_creation_form input[name=is-announcement-only]').prop('checked');
let stream_post_policy = parseInt($('#stream_creation_form input[name=stream-post-policy]').val(), 10);
const principals = get_principals();
let invite_only;
let history_public_to_subscribers;
// Because the stream_post_policy field is hidden when non-administrators create streams,
// we need to set the default value here.
if (isNaN(stream_post_policy)) {
stream_post_policy = stream_data.stream_post_policy_values.everyone.code;
}
if (privacy_setting === 'invite-only') {
invite_only = true;
history_public_to_subscribers = false;
@ -219,7 +225,7 @@ function create_stream() {
description,
principals,
invite_only,
is_announcement_only,
stream_post_policy,
announce,
history_public_to_subscribers
);

View File

@ -89,6 +89,21 @@ let filter_out_inactives = false;
const stream_ids_by_name = new FoldDict();
const default_stream_ids = new Set();
exports.stream_post_policy_values = {
everyone: {
code: 1,
description: i18n.t("All stream members can post"),
},
admins: {
code: 2,
description: i18n.t("Only organization administrators can post"),
},
non_new_members: {
code: 3,
description: i18n.t("Only organization full members can post"),
},
};
exports.clear_subscriptions = function () {
stream_info = new BinaryDict(function (sub) {
return sub.subscribed;
@ -375,8 +390,8 @@ exports.get_subscriber_count = function (stream_name) {
return sub.subscribers.size;
};
exports.update_stream_announcement_only = function (sub, is_announcement_only) {
sub.is_announcement_only = is_announcement_only;
exports.update_stream_post_policy = function (sub, stream_post_policy) {
sub.stream_post_policy = stream_post_policy;
};
exports.update_stream_privacy = function (sub, values) {
@ -505,12 +520,12 @@ exports.get_invite_only = function (stream_name) {
return sub.invite_only;
};
exports.get_announcement_only = function (stream_name) {
exports.get_stream_post_policy = function (stream_name) {
const sub = exports.get_sub(stream_name);
if (sub === undefined) {
return false;
}
return sub.is_announcement_only;
return sub.stream_post_policy;
};
exports.all_topics_in_cache = function (sub) {

View File

@ -272,6 +272,7 @@ exports.show_settings_for = function (node) {
const html = render_subscription_settings({
sub: sub,
settings: exports.stream_settings(sub),
stream_post_policy_values: stream_data.stream_post_policy_values,
});
ui.get_content_element($('.subscriptions .right .settings')).html(html);
@ -338,7 +339,6 @@ function stream_setting_clicked(e) {
}
}
exports.bulk_set_stream_property = function (sub_data) {
return channel.post({
url: '/json/users/me/subscriptions/properties',
@ -359,7 +359,7 @@ function change_stream_privacy(e) {
const sub = stream_data.get_sub_by_id(stream_id);
const privacy_setting = $('#stream_privacy_modal input[name=privacy]:checked').val();
const is_announcement_only = $('#stream_privacy_modal input[name=is-announcement-only]').prop('checked');
const stream_post_policy = parseInt($('#stream_privacy_modal input[name=stream-post-policy]:checked').val(), 10);
let invite_only;
let history_public_to_subscribers;
@ -380,7 +380,7 @@ function change_stream_privacy(e) {
stream_name: sub.name,
// toggle the privacy setting
is_private: JSON.stringify(invite_only),
is_announcement_only: JSON.stringify(is_announcement_only),
stream_post_policy: JSON.stringify(stream_post_policy),
history_public_to_subscribers: JSON.stringify(history_public_to_subscribers),
};
@ -493,7 +493,8 @@ exports.initialize = function () {
const template_data = {
stream_id: stream_id,
stream_name: stream.name,
is_announcement_only: stream.is_announcement_only,
stream_post_policy_values: stream_data.stream_post_policy_values,
stream_post_policy: stream.stream_post_policy,
is_public: !stream.invite_only,
is_private: stream.invite_only && !stream.history_public_to_subscribers,
is_private_with_public_history: stream.invite_only &&

View File

@ -53,8 +53,8 @@ exports.update_property = function (stream_id, property, value, other_values) {
case 'wildcard_mentions_notify':
update_stream_setting(sub, value, property);
break;
case 'is_announcement_only':
subs.update_stream_announcement_only(sub, value);
case 'stream_post_policy':
subs.update_stream_post_policy(sub, value);
break;
default:
blueslip.warn("Unexpected subscription property type", {property: property,

View File

@ -141,7 +141,7 @@ exports.update_stream_privacy_type_icon = function (sub) {
}
};
exports.update_stream_privacy_type_text = function (sub) {
exports.update_stream_subscription_type_text = function (sub) {
const stream_settings = stream_edit.settings_for_sub(sub);
const html = render_subscription_type(sub);
if (stream_edit.is_sub_settings_active(sub)) {

View File

@ -172,7 +172,7 @@ exports.update_stream_privacy = function (sub, values) {
// Update UI elements
stream_ui_updates.update_stream_privacy_type_icon(sub);
stream_ui_updates.update_stream_privacy_type_text(sub);
stream_ui_updates.update_stream_subscription_type_text(sub);
stream_ui_updates.update_change_stream_privacy_settings(sub);
stream_ui_updates.update_settings_button_for_sub(sub);
stream_ui_updates.update_subscribers_count(sub);
@ -180,11 +180,11 @@ exports.update_stream_privacy = function (sub, values) {
stream_list.redraw_stream_privacy(sub);
};
exports.update_stream_announcement_only = function (sub, new_value) {
stream_data.update_stream_announcement_only(sub, new_value);
exports.update_stream_post_policy = function (sub, new_value) {
stream_data.update_stream_post_policy(sub, new_value);
stream_data.update_calculated_fields(sub);
stream_ui_updates.update_stream_privacy_type_text(sub);
stream_ui_updates.update_stream_subscription_type_text(sub);
};
exports.set_color = function (stream_id, color) {
@ -549,6 +549,8 @@ exports.setup_page = function (callback) {
max_name_length: page_params.stream_name_max_length,
max_description_length: page_params.stream_description_max_length,
is_admin: page_params.is_admin,
stream_post_policy_values: stream_data.stream_post_policy_values,
stream_post_policy: stream_data.stream_post_policy_values.everyone.code,
};
const rendered = render_subscription_table_body(template_data);

View File

@ -27,7 +27,7 @@
{{t 'These settings are explained in detail in the <a target="_blank" href="/help/stream-permissions">help center</a>.'}}
</div>
{{> stream_types is_public=true }}
{{> stream_types is_public=true stream_post_policy=stream_post_policy_values.everyone.code}}
<div id="announce-new-stream">
<label class="checkbox">

View File

@ -1,4 +1,5 @@
<ul class="grey-box">
<h4>{{t 'Who can access the stream?'}}</h4>
<li>
<label class="radio">
<input type="radio" name="privacy" value="public" {{#if is_public}}checked{{/if}} />
@ -18,12 +19,14 @@
</label>
</li>
{{#if is_admin}}
<li>
<label class="checkbox">
<input type="checkbox" name="is-announcement-only" value="is-announcement-only" {{#if is_announcement_only}}checked{{/if}}/>
<span></span>
{{t 'Restrict posting to organization administrators' }}
</label>
</li>
<h4>{{t 'Who can post to the stream?'}}</h4>
{{#each stream_post_policy_values}}
<li>
<label class="radio">
<input type="radio" name="stream-post-policy" value="{{ this.code }}" {{#if (eq this.code ../stream_post_policy) }}checked{{/if}} />
{{ this.description }}
</label>
</li>
{{/each}}
{{/if}}
</ul>

View File

@ -41,7 +41,8 @@
</div>
<div class="subscription-type">
<div class="subscription-type-text">
{{> subscription_type}}
{{> subscription_type
stream_post_policy_values=../stream_post_policy_values}}
</div>
<a class="change-stream-privacy" {{#unless can_change_stream_permissions}}style="display: none;"{{/unless}}>[{{t "Change" }}]</a>
</div>

View File

@ -8,8 +8,10 @@
{{else}}
{{t 'This is a <i class="hash" aria-hidden="true"></i> <b>public stream</b>. Any member of the organization can join without an invitation.' }}
{{/if}}
{{#if is_announcement_only}}
{{#if (eq stream_post_policy stream_post_policy_values.admins.code)}}
{{t 'Only organization administrators can post.'}}
{{else if (eq stream_post_policy stream_post_policy_values.non_new_members.code)}}
{{t 'New members cannot post.'}}
{{else}}
{{t 'All stream members can post.'}}
{{/if}}

View File

@ -7,7 +7,7 @@ including:
* Stream [name](/help/rename-a-stream) and [description](/help/change-the-stream-description)
* Stream [permissions](/help/stream-permissions), including
[privacy](/help/change-the-privacy-of-a-stream) and [who can
send](/help/announcement-only-streams).
send](/help/stream-sending-policy).
`PATCH {{ api_url }}/v1/streams/{stream_id}`

View File

@ -157,4 +157,4 @@
* [Change a stream's description](/help/change-the-stream-description)
* [Change the privacy of a stream](/help/change-the-privacy-of-a-stream)
* [Add or remove users from a stream](/help/add-or-remove-users-from-a-stream)
* [Announcement-only streams](/help/announcement-only-streams)
* [Stream posting policy](/help/stream-sending-policy)

View File

@ -17,9 +17,9 @@ Zulip has many features designed to simplify moderation:
* Link to a code of conduct in your
[organization description](/help/create-your-organization-profile)
(displayed on the registration page).
* Create at least one
[default stream](/help/set-default-streams-for-new-users) where
[only admins can post](/help/announcement-only-streams).
* Create a [default stream](/help/set-default-streams-for-new-users)
for announcements where [only admins can
post](/help/stream-sending-policy).
* Add a [waiting period](/help/restrict-permissions-of-new-members) before
new users can take disruptive actions.
* [Restrict email visibility](/help/restrict-visibility-of-email-addresses)

View File

@ -16,6 +16,7 @@ Currently, the following actions support limiting access to full members.
- [Creating streams](/help/configure-who-can-create-streams)
- [Adding users to streams](/help/configure-who-can-invite-to-streams)
- [Restricting posting to a stream](/help/stream-sending-policy)
### Set waiting period for new members

View File

@ -3,8 +3,8 @@
Streams are similar to chatrooms, IRC channels, or email lists in that they
determine who receives a message. There are three types of streams in Zulip.
* **Public**: Anyone other than guests can join, and anyone (other than guests) can view the complete message
history without joining.
* **Public**: Anyone other than guests can join, and anyone (other
than guests) can view the complete message history without joining.
* **Private, shared history**: You must be added by a member of the stream. The
complete message history is available as soon as you are added.
@ -62,11 +62,9 @@ private stream messages:
&#9726; &nbsp; If subscribed to the stream
&#10038; Configurable. Org admins and Members can, by default, post to
any public stream, and Guests can only post to public streams if they
are subscribed. Additionally, streams can be configured to only allow
administrators to post.
&#10038; [Configurable](/help/stream-sending-policy). Org admins and
Members can, by default, post to any public stream, and Guests can
only post to public streams if they are subscribed.
### Private streams
@ -90,4 +88,5 @@ administrators to post.
&#9726; &nbsp; If subscribed to the stream
&#10038; Configurable, but at minimum must be subscribed to the stream
&#10038; [Configurable](/help/stream-sending-policy), but at minimum
must be subscribed to the stream.

View File

@ -1,13 +1,11 @@
# Announcement-only streams
# Stream posting policy
{!admin-only.md!}
By default, anyone who belongs to a stream can also send messages to the
stream. However, sometimes it's useful to have a stream (often a
By default, anyone who belongs to a stream can also send messages to
the stream. However, sometimes it's useful to have a stream (often a
[default stream](/help/set-default-streams-for-new-users)) where only
organization administrators can send messages.
### Restrict posting to organization administrators
certain users can send messages.
{start_tabs}
@ -18,7 +16,7 @@ organization administrators can send messages.
1. On the right, click **[Change]** next to the description of the stream
permissions.
1. Click **Restrict posting to organization administrators**.
1. Under "Who can post to the stream?", select the option you prefer.
1. Click **Save Changes**.

View File

@ -1824,7 +1824,7 @@ def create_stream_if_needed(realm: Realm,
stream_name: str,
*,
invite_only: bool=False,
is_announcement_only: bool=False,
stream_post_policy: int=Stream.STREAM_POST_POLICY_EVERYONE,
history_public_to_subscribers: Optional[bool]=None,
stream_description: str="") -> Tuple[Stream, bool]:
@ -1838,7 +1838,7 @@ def create_stream_if_needed(realm: Realm,
name=stream_name,
description=stream_description,
invite_only=invite_only,
is_announcement_only=is_announcement_only,
stream_post_policy=stream_post_policy,
history_public_to_subscribers=history_public_to_subscribers,
is_in_zephyr_realm=realm.is_zephyr_mirror_realm
)
@ -1878,7 +1878,7 @@ def create_streams_if_needed(realm: Realm,
realm,
stream_dict["name"],
invite_only=stream_dict.get("invite_only", False),
is_announcement_only=stream_dict.get("is_announcement_only", False),
stream_post_policy=stream_dict.get("stream_post_policy", Stream.STREAM_POST_POLICY_EVERYONE),
history_public_to_subscribers=stream_dict.get("history_public_to_subscribers"),
stream_description=stream_dict.get("description", "")
)
@ -2243,14 +2243,20 @@ def validate_sender_can_write_to_stream(sender: UserProfile,
# Our caller is responsible for making sure that `stream` actually
# matches the realm of the sender.
if stream.is_announcement_only:
if sender.is_realm_admin or is_cross_realm_bot_email(sender.delivery_email):
pass
elif sender.is_bot and (sender.bot_owner is not None and
sender.bot_owner.is_realm_admin):
pass
else:
raise JsonableError(_("Only organization administrators can send to this stream."))
# Organization admins can send to any stream, irrespective of the stream_post_policy value.
if sender.is_realm_admin or is_cross_realm_bot_email(sender.delivery_email):
pass
elif sender.is_bot and (sender.bot_owner is not None and
sender.bot_owner.is_realm_admin):
pass
elif stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS:
raise JsonableError(_("Only organization administrators can send to this stream."))
elif stream.stream_post_policy == Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS:
if sender.is_bot and (sender.bot_owner is not None and
sender.bot_owner.is_new_member):
raise JsonableError(_("New members cannot send to this stream."))
elif sender.is_new_member:
raise JsonableError(_("New members cannot send to this stream."))
if not (stream.invite_only or sender.is_guest):
# This is a public stream and sender is not a guest user
@ -3632,14 +3638,28 @@ def do_change_stream_web_public(stream: Stream, is_web_public: bool) -> None:
stream.is_web_public = is_web_public
stream.save(update_fields=['is_web_public'])
def do_change_stream_announcement_only(stream: Stream, is_announcement_only: bool) -> None:
stream.is_announcement_only = is_announcement_only
stream.save(update_fields=['is_announcement_only'])
def do_change_stream_post_policy(stream: Stream, stream_post_policy: int) -> None:
stream.stream_post_policy = stream_post_policy
stream.save(update_fields=['stream_post_policy'])
event = dict(
op="update",
type="stream",
property="stream_post_policy",
value=stream_post_policy,
stream_id=stream.id,
name=stream.name,
)
send_event(stream.realm, event, can_access_stream_user_ids(stream))
# Backwards-compatibility code: We removed the
# is_announcement_only property in early 2020, but we send a
# duplicate event for legacy mobile clients that might want the
# data.
event = dict(
op="update",
type="stream",
property="is_announcement_only",
value=is_announcement_only,
value=stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS,
stream_id=stream.id,
name=stream.name,
)
@ -4818,6 +4838,11 @@ def gather_subscriptions_helper(user_profile: UserProfile,
# Backwards-compatibility for clients that haven't been
# updated for the in_home_view => is_muted API migration.
stream_dict['in_home_view'] = not stream_dict['is_muted']
# Backwards-compatibility for clients that haven't been
# updated for the is_announcement_only -> stream_post_policy
# migration.
stream_dict['is_announcement_only'] = \
stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS
# Add a few computed fields not directly from the data models.
stream_dict['is_old_stream'] = is_old_stream(stream["date_created"])
@ -4866,6 +4891,9 @@ def gather_subscriptions_helper(user_profile: UserProfile,
stream_dict['is_old_stream'] = is_old_stream(stream["date_created"])
stream_dict['stream_weekly_traffic'] = get_average_weekly_stream_traffic(
stream["id"], stream["date_created"], recent_traffic)
# Backwards-compatibility addition of removed field.
stream_dict['is_announcement_only'] = \
stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS
if is_public or user_profile.is_realm_admin:
subscribers = subscriber_map[stream["id"]]

View File

@ -113,7 +113,8 @@ def bulk_create_streams(realm: Realm,
description=options["description"],
rendered_description=render_stream_description(options["description"]),
invite_only=options.get("invite_only", False),
is_announcement_only=options.get("is_announcement_only", False),
stream_post_policy=options.get("stream_post_policy",
Stream.STREAM_POST_POLICY_EVERYONE),
history_public_to_subscribers=options["history_public_to_subscribers"],
is_web_public=options.get("is_web_public", False),
is_in_zephyr_realm=realm.is_zephyr_mirror_realm,

View File

@ -477,7 +477,7 @@ def apply_event(state: Dict[str, Any],
stream_data['subscribers'] = []
stream_data['stream_weekly_traffic'] = None
stream_data['is_old_stream'] = False
stream_data['is_announcement_only'] = False
stream_data['stream_post_policy'] = Stream.STREAM_POST_POLICY_EVERYONE
# Add stream to never_subscribed (if not invite_only)
state['never_subscribed'].append(stream_data)
state['streams'].append(stream)

View File

@ -261,8 +261,16 @@ def list_to_streams(streams_raw: Iterable[Mapping[str, Any]],
stream_name = stream_dict["name"]
stream = existing_stream_map.get(stream_name.lower())
if stream is None:
if stream_dict.get("is_announcement_only", False) and not user_profile.is_realm_admin:
# Non admins cannot create STREAM_POST_POLICY_ADMINS streams.
if ((stream_dict.get("stream_post_policy", False) ==
Stream.STREAM_POST_POLICY_ADMINS) and not user_profile.is_realm_admin):
member_creating_announcement_only_stream = True
# New members cannot create STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams,
# unless they are admins who are also new members of the organization.
if ((stream_dict.get("stream_post_policy", False) ==
Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS) and user_profile.is_new_member):
if not user_profile.is_realm_admin:
member_creating_announcement_only_stream = True
missing_stream_dicts.append(stream_dict)
else:
existing_streams.append(stream)

View File

@ -292,12 +292,12 @@ class ZulipTestCase(TestCase):
def notification_bot(self) -> UserProfile:
return get_system_bot(settings.NOTIFICATION_BOT)
def create_test_bot(self, short_name: str, user_profile: UserProfile,
def create_test_bot(self, short_name: str, user_profile: UserProfile, full_name: str='Foo Bot',
assert_json_error_msg: str=None, **extras: Any) -> Optional[UserProfile]:
self.login(user_profile.delivery_email)
bot_info = {
'short_name': short_name,
'full_name': 'Foo Bot',
'full_name': full_name,
}
bot_info.update(extras)
result = self.client_post("/json/bots", bot_info)

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2020-01-27 22:03
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('zerver', '0262_mutedtopic_date_muted'),
]
operations = [
migrations.AddField(
model_name='stream',
name='stream_post_policy',
field=models.PositiveSmallIntegerField(default=1),
),
]

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2020-01-25 23:47
from __future__ import unicode_literals
from django.db import migrations
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
from django.db.migrations.state import StateApps
def upgrade_stream_post_policy(apps: StateApps, schema_editor: DatabaseSchemaEditor) -> None:
STREAM_POST_POLICY_EVERYONE = 1
STREAM_POST_POLICY_ADMINS = 2
Stream = apps.get_model('zerver', 'Stream')
Stream.objects.filter(is_announcement_only=False) \
.update(stream_post_policy=STREAM_POST_POLICY_EVERYONE)
Stream.objects.filter(is_announcement_only=True) \
.update(stream_post_policy=STREAM_POST_POLICY_ADMINS)
class Migration(migrations.Migration):
dependencies = [
('zerver', '0263_stream_stream_post_policy'),
]
operations = [
migrations.RunPython(upgrade_stream_post_policy,
reverse_code=migrations.RunPython.noop),
]

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.26 on 2020-01-27 22:05
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('zerver', '0264_migrate_is_announcement_only'),
]
operations = [
migrations.RemoveField(
model_name='stream',
name='is_announcement_only',
),
]

View File

@ -1373,8 +1373,18 @@ class Stream(models.Model):
# Whether this stream's content should be published by the web-public archive features
is_web_public = models.BooleanField(default=False) # type: bool
# Whether only organization administrators can send messages to this stream
is_announcement_only = models.BooleanField(default=False) # type: bool
STREAM_POST_POLICY_EVERYONE = 1
STREAM_POST_POLICY_ADMINS = 2
STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS = 3
# TODO: Implement policy to restrict posting to a user group or admins.
# Who in the organization has permission to send messages to this stream.
stream_post_policy = models.PositiveSmallIntegerField(default=STREAM_POST_POLICY_EVERYONE) # type: int
STREAM_POST_POLICY_TYPES = [
STREAM_POST_POLICY_EVERYONE,
STREAM_POST_POLICY_ADMINS,
STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS,
]
# The unique thing about Zephyr public streams is that we never list their
# users. We may try to generalize this concept later, but for now
@ -1434,7 +1444,7 @@ class Stream(models.Model):
"rendered_description",
"invite_only",
"is_web_public",
"is_announcement_only",
"stream_post_policy",
"history_public_to_subscribers",
"first_message_id",
]
@ -1447,6 +1457,7 @@ class Stream(models.Model):
result['stream_id'] = self.id
continue
result[field_name] = getattr(self, field_name)
result['is_announcement_only'] = self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS
return result
post_save.connect(flush_stream, sender=Stream)

View File

@ -328,7 +328,7 @@ def update_stream(client, stream_id):
# Update the stream by a given ID
request = {
'stream_id': stream_id,
'is_announcement_only': True,
'stream_post_policy': 2,
'is_private': True,
}

View File

@ -1504,14 +1504,13 @@ paths:
type: boolean
default: None
example: false
- name: is_announcement_only
- name: stream_post_policy
in: query
description: A boolean indicating if the stream is an announcements only stream.
Only organization admins can post to announcements only streams.
description: Indicates which users can post messages to the stream.
schema:
type: boolean
default: false
example: false
type: integer
default: 1
example: 1
- name: announce
in: query
description: If `announce` is `True` and one of the streams specified
@ -2624,6 +2623,13 @@ paths:
type: boolean
example: true
required: false
- name: stream_post_policy
in: query
description: Indicates which users can post messages to the stream.
schema:
type: integer
example: 2
required: false
- name: history_public_to_subscribers
in: query
description: The new state for the history_public_to_subscribers.

View File

@ -52,7 +52,7 @@ from zerver.lib.actions import (
do_change_realm_domain,
do_change_stream_description,
do_change_stream_invite_only,
do_change_stream_announcement_only,
do_change_stream_post_policy,
do_change_subscription_property,
do_change_user_delivery_email,
do_create_user,
@ -134,7 +134,7 @@ from zerver.lib.topic_mutes import (
)
from zerver.lib.validator import (
check_bool, check_dict, check_dict_only, check_float, check_int, check_list, check_string,
equals, check_none_or, Validator, check_url
equals, check_none_or, Validator, check_url, check_int_in
)
from zerver.lib.users import get_api_key
@ -1438,6 +1438,7 @@ class EventsRegisterTest(ZulipTestCase):
('invite_only', check_bool),
('is_web_public', check_bool),
('is_announcement_only', check_bool),
('stream_post_policy', check_int_in(Stream.STREAM_POST_POLICY_TYPES)),
('name', check_string),
('stream_id', check_int),
('first_message_id', check_none_or(check_int)),
@ -2508,6 +2509,7 @@ class EventsRegisterTest(ZulipTestCase):
('invite_only', check_bool),
('is_web_public', check_bool),
('is_announcement_only', check_bool),
('stream_post_policy', check_int_in(Stream.STREAM_POST_POLICY_TYPES)),
('is_muted', check_bool),
('in_home_view', check_bool),
('name', check_string),
@ -2584,13 +2586,13 @@ class EventsRegisterTest(ZulipTestCase):
('value', check_bool),
('history_public_to_subscribers', check_bool),
])
stream_update_is_announcement_only_schema_checker = self.check_events_dict([
stream_update_stream_post_policy_schema_checker = self.check_events_dict([
('type', equals('stream')),
('op', equals('update')),
('property', equals('is_announcement_only')),
('property', equals('stream_post_policy')),
('stream_id', check_int),
('name', check_string),
('value', check_bool),
('value', check_int_in(Stream.STREAM_POST_POLICY_TYPES)),
])
# Subscribe to a totally new stream, so it's just Hamlet on it
@ -2655,11 +2657,11 @@ class EventsRegisterTest(ZulipTestCase):
error = stream_update_invite_only_schema_checker('events[0]', events[0])
self.assert_on_error(error)
# Update stream is_announcement_only property
action = lambda: do_change_stream_announcement_only(stream, True)
# Update stream stream_post_policy property
action = lambda: do_change_stream_post_policy(stream, Stream.STREAM_POST_POLICY_ADMINS)
events = self.do_test(action,
include_subscribers=include_subscribers)
error = stream_update_is_announcement_only_schema_checker('events[0]', events[0])
include_subscribers=include_subscribers, num_events=2)
error = stream_update_stream_post_policy_schema_checker('events[0]', events[0])
self.assert_on_error(error)
# Subscribe to a totally new invite-only stream, so it's just Hamlet on it

View File

@ -1424,6 +1424,15 @@ class SewMessageAndReactionTest(ZulipTestCase):
class MessagePOSTTest(ZulipTestCase):
def _send_and_verify_message(self, email: str, stream_name: str, error_msg: str=None) -> None:
if error_msg is None:
msg_id = self.send_stream_message(email, stream_name)
result = self.client_get('/json/messages/' + str(msg_id))
self.assert_json_success(result)
else:
with self.assertRaisesRegex(JsonableError, error_msg):
self.send_stream_message(email, stream_name)
def test_message_to_self(self) -> None:
"""
Sending a message to a stream to which you are subscribed is
@ -1489,36 +1498,52 @@ class MessagePOSTTest(ZulipTestCase):
sent_message = self.get_last_message()
self.assertEqual(sent_message.content, "Stream message by ID.")
def test_message_to_announce(self) -> None:
def test_sending_message_as_stream_post_policy_admins(self) -> None:
"""
Sending a message to an announcement_only stream by a realm admin
successful.
Sending messages to streams which only the admins can create and post to.
"""
user_profile = self.example_user("iago")
self.login(user_profile.email)
admin_profile = self.example_user("iago")
self.login(admin_profile.email)
stream_name = "Verona"
stream = get_stream(stream_name, user_profile.realm)
stream.is_announcement_only = True
stream = get_stream(stream_name, admin_profile.realm)
stream.stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS
stream.save()
result = self.client_post("/json/messages", {"type": "stream",
"to": stream_name,
"client": "test suite",
"content": "Test message",
"topic": "Test topic"})
self.assert_json_success(result)
# Admins and their owned bots can send to STREAM_POST_POLICY_ADMINS streams
self._send_and_verify_message(admin_profile.email, stream_name)
admin_owned_bot = self.create_test_bot(
short_name='whatever',
user_profile=user_profile,
short_name='whatever1',
full_name='whatever1',
user_profile=admin_profile,
)
result = self.api_post(admin_owned_bot.email,
"/api/v1/messages", {"type": "stream",
"to": stream_name,
"client": "test suite",
"content": "Test message",
"topic": "Test topic"})
self.assert_json_success(result)
self._send_and_verify_message(admin_owned_bot.email, stream_name)
non_admin_profile = self.example_user("hamlet")
self.login(non_admin_profile.email)
# Non admins and their owned bots cannot send to STREAM_POST_POLICY_ADMINS streams
self._send_and_verify_message(non_admin_profile.email, stream_name,
"Only organization administrators can send to this stream.")
non_admin_owned_bot = self.create_test_bot(
short_name='whatever2',
full_name='whatever2',
user_profile=non_admin_profile,
)
self._send_and_verify_message(non_admin_owned_bot.email, stream_name,
"Only organization administrators can send to this stream.")
# Bots without owner (except cross realm bot) cannot send to announcement only streams
bot_without_owner = do_create_user(
email='free-bot@zulip.testserver',
password='',
realm=non_admin_profile.realm,
full_name='freebot',
short_name='freebot',
bot_type=UserProfile.DEFAULT_BOT,
)
self._send_and_verify_message(bot_without_owner.email, stream_name,
"Only organization administrators can send to this stream.")
# Cross realm bots should be allowed (through internal_send_message)
notification_bot = get_system_bot("notification-bot@zulip.com")
@ -1526,54 +1551,71 @@ class MessagePOSTTest(ZulipTestCase):
'Test topic', 'Test message by notification bot')
self.assertEqual(self.get_last_message().content, 'Test message by notification bot')
def test_message_fail_to_announce(self) -> None:
def test_sending_message_as_stream_post_policy_restrict_new_members(self) -> None:
"""
Sending a message to an announcement_only stream not by a realm
admin fails.
Sending messages to streams which new members cannot create and post to.
"""
user_profile = self.example_user("hamlet")
self.login(user_profile.email)
admin_profile = self.example_user("iago")
self.login(admin_profile.email)
do_set_realm_property(admin_profile.realm, 'waiting_period_threshold', 10)
admin_profile.date_joined = timezone_now() - datetime.timedelta(days=9)
admin_profile.save()
self.assertTrue(admin_profile.is_new_member)
self.assertTrue(admin_profile.is_realm_admin)
stream_name = "Verona"
stream = get_stream(stream_name, user_profile.realm)
stream.is_announcement_only = True
stream = get_stream(stream_name, admin_profile.realm)
stream.stream_post_policy = Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS
stream.save()
result = self.client_post("/json/messages", {"type": "stream",
"to": stream_name,
"client": "test suite",
"content": "Test message",
"topic": "Test topic"})
self.assert_json_error(result, "Only organization administrators can send to this stream.")
# Non admin owned bot fail to send to announcement only stream
non_admin_owned_bot = self.create_test_bot(
short_name='whatever',
user_profile=user_profile,
# Admins and their owned bots can send to STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams,
# even if the admin is a new user
self._send_and_verify_message(admin_profile.email, stream_name)
admin_owned_bot = self.create_test_bot(
short_name='whatever1',
full_name='whatever1',
user_profile=admin_profile,
)
result = self.api_post(non_admin_owned_bot.email,
"/api/v1/messages", {"type": "stream",
"to": stream_name,
"client": "test suite",
"content": "Test message",
"topic": "Test topic"})
self.assert_json_error(result, "Only organization administrators can send to this stream.")
self._send_and_verify_message(admin_owned_bot.email, stream_name)
# Bots without owner (except cross realm bot) fail to send to announcement only stream
non_admin_profile = self.example_user("hamlet")
self.login(non_admin_profile.email)
non_admin_profile.date_joined = timezone_now() - datetime.timedelta(days=9)
non_admin_profile.save()
self.assertTrue(non_admin_profile.is_new_member)
self.assertFalse(non_admin_profile.is_realm_admin)
# Non admins and their owned bots can send to STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams,
# if the user is not a new member
self._send_and_verify_message(non_admin_profile.email, stream_name,
"New members cannot send to this stream.")
non_admin_owned_bot = self.create_test_bot(
short_name='whatever2',
full_name='whatever2',
user_profile=non_admin_profile,
)
self._send_and_verify_message(non_admin_owned_bot.email, stream_name,
"New members cannot send to this stream.")
# Bots without owner (except cross realm bot) cannot send to announcement only stream
bot_without_owner = do_create_user(
email='free-bot@zulip.testserver',
password='',
realm=user_profile.realm,
realm=non_admin_profile.realm,
full_name='freebot',
short_name='freebot',
bot_type=UserProfile.DEFAULT_BOT,
)
result = self.api_post(bot_without_owner.email,
"/api/v1/messages", {"type": "stream",
"to": stream_name,
"client": "test suite",
"content": "Test message",
"topic": "Test topic"})
self.assert_json_error(result, "Only organization administrators can send to this stream.")
self._send_and_verify_message(bot_without_owner.email, stream_name,
"New members cannot send to this stream.")
# Cross realm bots should be allowed (through internal_send_message)
notification_bot = get_system_bot("notification-bot@zulip.com")
internal_send_stream_message(stream.realm, notification_bot, stream,
'Test topic', 'Test message by notification bot')
self.assertEqual(self.get_last_message().content, 'Test message by notification bot')
def test_api_message_with_default_to(self) -> None:
"""

View File

@ -142,7 +142,7 @@ class TestCreateStreams(ZulipTestCase):
[{"name": stream_name,
"description": stream_description,
"invite_only": True,
"is_announcement_only": True}
"stream_post_policy": Stream.STREAM_POST_POLICY_ADMINS}
for (stream_name, stream_description) in zip(stream_names, stream_descriptions)])
self.assertEqual(len(new_streams), 3)
@ -154,7 +154,7 @@ class TestCreateStreams(ZulipTestCase):
self.assertEqual(actual_stream_descriptions, set(stream_descriptions))
for stream in new_streams:
self.assertTrue(stream.invite_only)
self.assertTrue(stream.is_announcement_only)
self.assertTrue(stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS)
new_streams, existing_streams = create_streams_if_needed(
realm,
@ -264,7 +264,7 @@ class TestCreateStreams(ZulipTestCase):
"invite_only": 'false',
"announce": 'true',
"principals": '["iago@zulip.com", "AARON@zulip.com", "cordelia@zulip.com", "hamlet@zulip.com"]',
"is_announcement_only": 'false'
"stream_post_policy": '1'
}
response = self.client_post("/json/users/me/subscriptions", data)
@ -780,7 +780,7 @@ class StreamAdminTest(ZulipTestCase):
{'description': ujson.dumps('Test description')})
self.assert_json_error(result, 'Must be an organization administrator')
def test_change_stream_announcement_only(self) -> None:
def test_change_to_stream_post_policy_admins(self) -> None:
user_profile = self.example_user('hamlet')
self.login(user_profile.email)
@ -792,9 +792,9 @@ class StreamAdminTest(ZulipTestCase):
{'is_announcement_only': ujson.dumps(True)})
self.assert_json_success(result)
stream = get_stream('stream_name1', user_profile.realm)
self.assertEqual(True, stream.is_announcement_only)
self.assertTrue(stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS)
def test_change_stream_announcement_only_requires_realm_admin(self) -> None:
def test_change_stream_post_policy_requires_realm_admin(self) -> None:
user_profile = self.example_user('hamlet')
self.login(user_profile.email)
@ -803,7 +803,17 @@ class StreamAdminTest(ZulipTestCase):
stream_id = get_stream('stream_name1', user_profile.realm).id
result = self.client_patch('/json/streams/%d' % (stream_id,),
{'is_announcement_only': ujson.dumps(True)})
{'stream_post_policy': ujson.dumps(
Stream.STREAM_POST_POLICY_ADMINS)})
self.assert_json_error(result, 'Must be an organization administrator')
do_set_realm_property(user_profile.realm, 'waiting_period_threshold', 10)
self.assertTrue(user_profile.is_new_member)
stream_id = get_stream('stream_name1', user_profile.realm).id
result = self.client_patch('/json/streams/%d' % (stream_id,),
{'stream_post_policy': ujson.dumps(
Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS)})
self.assert_json_error(result, 'Must be an organization administrator')
def set_up_stream_for_deletion(self, stream_name: str, invite_only: bool=False,
@ -2570,7 +2580,7 @@ class SubscriptionAPITest(ZulipTestCase):
self.assertEqual(add_event['event']['op'], 'add')
self.assertEqual(add_event['users'], [self.example_user("iago").id])
def test_subscibe_to_announce_only_stream(self) -> None:
def test_subscribe_to_stream_post_policy_admins_stream(self) -> None:
"""
Members can subscribe to streams where only admins can post
but not create those streams, only realm admins can
@ -2581,7 +2591,7 @@ class SubscriptionAPITest(ZulipTestCase):
streams_raw = [{
'name': 'new_stream',
'is_announcement_only': True,
'stream_post_policy': Stream.STREAM_POST_POLICY_ADMINS,
}]
with self.assertRaisesRegex(
JsonableError, "User cannot create a stream with these settings."):
@ -2592,7 +2602,56 @@ class SubscriptionAPITest(ZulipTestCase):
self.assert_length(result[0], 0)
self.assert_length(result[1], 1)
self.assertEqual(result[1][0].name, 'new_stream')
self.assertEqual(result[1][0].is_announcement_only, True)
self.assertTrue(result[1][0].stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS)
def test_subscribe_to_stream_post_policy_restrict_new_members_stream(self) -> None:
"""
New members can subscribe to streams where they can neither post
nor create those streams, only realm admins can can.
"""
new_member_email = self.nonreg_email('test')
self.register(new_member_email, "test")
new_member = self.nonreg_user('test')
do_set_realm_property(new_member.realm, 'waiting_period_threshold', 10)
streams_raw = [{
'name': 'new_stream',
'stream_post_policy': Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS,
}]
# new members cannot create STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams.
self.assertTrue(new_member.is_new_member)
with self.assertRaisesRegex(
JsonableError, "User cannot create a stream with these settings."):
list_to_streams(streams_raw, new_member, autocreate=True)
# Non admins can create STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams.
# However, they must not be a new user.
non_admin = self.example_user("AARON")
non_admin.date_joined = timezone_now() - timedelta(days=11)
non_admin.save()
self.assertFalse(non_admin.is_new_member)
self.assertFalse(non_admin.is_realm_admin)
result = list_to_streams(streams_raw, non_admin, autocreate=True)
self.assert_length(result[0], 0)
self.assert_length(result[1], 1)
self.assertEqual(result[1][0].name, 'new_stream')
self.assertTrue(result[1][0].stream_post_policy == Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS)
streams_raw = [{
'name': 'newer_stream',
'stream_post_policy': Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS,
}]
# Admins can create STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams,
# irrespective of whether they are new members or not.
admin = self.example_user("iago")
self.assertTrue(admin.is_realm_admin)
result = list_to_streams(streams_raw, admin, autocreate=True)
self.assert_length(result[0], 0)
self.assert_length(result[1], 1)
self.assertEqual(result[1][0].name, 'newer_stream')
self.assertTrue(result[1][0].stream_post_policy == Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS)
def test_guest_user_subscribe(self) -> None:
"""Guest users cannot subscribe themselves to anything"""
@ -2611,7 +2670,7 @@ class SubscriptionAPITest(ZulipTestCase):
'invite_only': False,
'history_public_to_subscribers': None,
'name': 'new_stream',
'is_announcement_only': False
'stream_post_policy': Stream.STREAM_POST_POLICY_EVERYONE
}]
with self.assertRaisesRegex(JsonableError, "User cannot create streams."):

View File

@ -17,19 +17,18 @@ from zerver.lib.actions import bulk_remove_subscriptions, \
bulk_add_subscriptions, do_send_messages, get_subscriber_emails, do_rename_stream, \
do_deactivate_stream, do_change_stream_invite_only, do_add_default_stream, \
do_change_stream_description, do_get_streams, \
do_remove_default_stream, \
do_remove_default_stream, do_change_stream_post_policy, do_delete_messages, \
do_create_default_stream_group, do_add_streams_to_default_stream_group, \
do_remove_streams_from_default_stream_group, do_remove_default_stream_group, \
do_change_default_stream_group_description, do_change_default_stream_group_name, \
do_change_stream_announcement_only, \
do_delete_messages
do_change_default_stream_group_description, do_change_default_stream_group_name
from zerver.lib.response import json_success, json_error
from zerver.lib.streams import access_stream_by_id, access_stream_by_name, \
check_stream_name, check_stream_name_available, filter_stream_authorization, \
list_to_streams, access_stream_for_delete_or_update, access_default_stream_group_by_id
from zerver.lib.topic import get_topic_history_for_stream, messages_for_topic
from zerver.lib.validator import check_string, check_int, check_list, check_dict, \
check_bool, check_variable_type, check_capped_string, check_color, check_dict_only
check_bool, check_variable_type, check_capped_string, check_color, check_dict_only, \
check_int_in
from zerver.models import UserProfile, Stream, Realm, UserMessage, \
get_system_bot, get_active_user
@ -150,6 +149,8 @@ def update_stream_backend(
Stream.MAX_DESCRIPTION_LENGTH), default=None),
is_private: Optional[bool]=REQ(validator=check_bool, default=None),
is_announcement_only: Optional[bool]=REQ(validator=check_bool, default=None),
stream_post_policy: Optional[int]=REQ(validator=check_int_in(
Stream.STREAM_POST_POLICY_TYPES), default=None),
history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None),
new_name: Optional[str]=REQ(validator=check_string, default=None),
) -> HttpResponse:
@ -171,7 +172,15 @@ def update_stream_backend(
check_stream_name_available(user_profile.realm, new_name)
do_rename_stream(stream, new_name, user_profile)
if is_announcement_only is not None:
do_change_stream_announcement_only(stream, is_announcement_only)
# is_announcement_only is a legacy way to specify
# stream_post_policy. We can probably just delete this code,
# since we're not aware of clients that used it, but we're
# keeping it for backwards-compatibility for now.
stream_post_policy = Stream.STREAM_POST_POLICY_EVERYONE
if is_announcement_only:
stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS
if stream_post_policy is not None:
do_change_stream_post_policy(stream, stream_post_policy)
# But we require even realm administrators to be actually
# subscribed to make a private stream public.
@ -296,7 +305,8 @@ def add_subscriptions_backend(
])
)),
invite_only: bool=REQ(validator=check_bool, default=False),
is_announcement_only: bool=REQ(validator=check_bool, default=False),
stream_post_policy: int=REQ(validator=check_int_in(
Stream.STREAM_POST_POLICY_TYPES), default=Stream.STREAM_POST_POLICY_EVERYONE),
history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None),
announce: bool=REQ(validator=check_bool, default=False),
principals: List[str]=REQ(validator=check_list(check_string), default=[]),
@ -319,7 +329,7 @@ def add_subscriptions_backend(
# Strip the stream name here.
stream_dict_copy['name'] = stream_dict_copy['name'].strip()
stream_dict_copy["invite_only"] = invite_only
stream_dict_copy["is_announcement_only"] = is_announcement_only
stream_dict_copy["stream_post_policy"] = stream_post_policy
stream_dict_copy["history_public_to_subscribers"] = history_public_to_subscribers
stream_dicts.append(stream_dict_copy)

View File

@ -526,7 +526,8 @@ class Command(BaseCommand):
zulip_stream_dict = {
"devel": {"description": "For developing"},
"all": {"description": "For **everything**"},
"announce": {"description": "For announcements", 'is_announcement_only': True},
"announce": {"description": "For announcements",
'stream_post_policy': Stream.STREAM_POST_POLICY_ADMINS},
"design": {"description": "For design"},
"support": {"description": "For support"},
"social": {"description": "For socializing"},