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'; 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 = { const me = {
email: 'me@example.com', email: 'me@example.com',
user_id: 30, user_id: 30,
@ -100,6 +107,7 @@ const bob = {
full_name: 'Bob', full_name: 'Bob',
}; };
people.add(new_user);
people.add(me); people.add(me);
people.initialize_current_user(me.user_id); people.initialize_current_user(me.user_id);
@ -315,30 +323,30 @@ run_test('validate_stream_message', () => {
assert($("#compose-all-everyone").visible()); 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 // 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. // 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 // 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; page_params.is_admin = false;
const sub = { const sub = {
stream_id: 102, stream_id: 102,
name: 'stream102', name: 'stream102',
subscribed: true, subscribed: true,
announcement_only: true, stream_post_policy: stream_data.stream_post_policy_values.admins.code,
}; };
stream_data.get_announcement_only = function () { stream_data.get_stream_post_policy = function () {
return true; return 2;
}; };
compose_state.topic('subject102'); compose_state.topic('subject102');
stream_data.add_sub('stream102', sub); stream_data.add_sub('stream102', sub);
assert(!compose.validate()); assert(!compose.validate());
assert.equal($('#compose-error-msg').html(), i18n.t("Only organization admins are allowed to post to this stream.")); 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. // do not reproduce this error.
stream_data.get_announcement_only = function () { stream_data.get_stream_post_policy = function () {
return false; 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('compose_pm_pill', {
}); });
set_global('i18n', global.stub_i18n);
zrequire('people'); zrequire('people');
zrequire('compose_ui'); zrequire('compose_ui');
zrequire('compose'); zrequire('compose');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ const noop = function () {};
const return_true = function () { return true; }; const return_true = function () { return true; };
set_global('$', global.make_zjquery()); set_global('$', global.make_zjquery());
set_global('document', 'document-stub'); set_global('document', 'document-stub');
set_global('i18n', global.stub_i18n);
set_global('colorspace', { set_global('colorspace', {
sRGB_to_linear: noop, 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) { with_overrides(function (override) {
global.with_stub(function (stub) { global.with_stub(function (stub) {
override('subs.update_stream_announcement_only', stub.f); override('subs.update_stream_post_policy', stub.f);
stream_events.update_property(1, 'is_announcement_only', true); stream_events.update_property(1, 'stream_post_policy', stream_data.stream_post_policy_values.admins.code);
const args = stub.get_args('sub', 'val'); const args = stub.get_args('sub', 'val');
assert.equal(args.sub.stream_id, 1); 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('document', 'document-stub');
set_global('$', global.make_zjquery()); set_global('$', global.make_zjquery());
set_global('blueslip', global.make_zblueslip()); set_global('blueslip', global.make_zblueslip());
set_global('i18n', global.stub_i18n);
const FoldDict = zrequire('fold_dict').FoldDict; const FoldDict = zrequire('fold_dict').FoldDict;
const IntDict = zrequire('int_dict').IntDict; const IntDict = zrequire('int_dict').IntDict;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -477,13 +477,30 @@ function validate_stream_message_announce(stream_name) {
return true; return true;
} }
function validate_stream_message_announcement_only(stream_name) { function validate_stream_message_post_policy(stream_name) {
// Only allow realm admins to post to announcement_only streams. if (page_params.is_admin) {
const is_announcement_only = stream_data.get_announcement_only(stream_name); return true;
if (is_announcement_only && !page_params.is_admin) { }
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.")); compose_error(i18n.t("Only organization admins are allowed to post to this stream."));
return false; 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; 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; return false;
} }

View File

@ -98,7 +98,7 @@ const stream_name_error = (function () {
}()); }());
function ajaxSubscribeForCreation(stream_name, description, user_ids, invite_only, 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 // TODO: We can eliminate the user_ids -> principals conversion
// once we upgrade the backend to accept user_ids. // once we upgrade the backend to accept user_ids.
const persons = _.compact(_.map(user_ids, (user_id) => { const persons = _.compact(_.map(user_ids, (user_id) => {
@ -114,7 +114,7 @@ function ajaxSubscribeForCreation(stream_name, description, user_ids, invite_onl
description: description}]), description: description}]),
principals: JSON.stringify(principals), principals: JSON.stringify(principals),
invite_only: JSON.stringify(invite_only), 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), announce: JSON.stringify(announce),
history_public_to_subscribers: JSON.stringify(history_public_to_subscribers), 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 stream_name = $.trim($("#create_stream_name").val());
const description = $.trim($("#create_stream_description").val()); const description = $.trim($("#create_stream_description").val());
const privacy_setting = $('#stream_creation_form input[name=privacy]:checked').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(); const principals = get_principals();
let invite_only; let invite_only;
let history_public_to_subscribers; 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') { if (privacy_setting === 'invite-only') {
invite_only = true; invite_only = true;
history_public_to_subscribers = false; history_public_to_subscribers = false;
@ -219,7 +225,7 @@ function create_stream() {
description, description,
principals, principals,
invite_only, invite_only,
is_announcement_only, stream_post_policy,
announce, announce,
history_public_to_subscribers history_public_to_subscribers
); );

View File

@ -89,6 +89,21 @@ let filter_out_inactives = false;
const stream_ids_by_name = new FoldDict(); const stream_ids_by_name = new FoldDict();
const default_stream_ids = new Set(); 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 () { exports.clear_subscriptions = function () {
stream_info = new BinaryDict(function (sub) { stream_info = new BinaryDict(function (sub) {
return sub.subscribed; return sub.subscribed;
@ -375,8 +390,8 @@ exports.get_subscriber_count = function (stream_name) {
return sub.subscribers.size; return sub.subscribers.size;
}; };
exports.update_stream_announcement_only = function (sub, is_announcement_only) { exports.update_stream_post_policy = function (sub, stream_post_policy) {
sub.is_announcement_only = is_announcement_only; sub.stream_post_policy = stream_post_policy;
}; };
exports.update_stream_privacy = function (sub, values) { exports.update_stream_privacy = function (sub, values) {
@ -505,12 +520,12 @@ exports.get_invite_only = function (stream_name) {
return sub.invite_only; 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); const sub = exports.get_sub(stream_name);
if (sub === undefined) { if (sub === undefined) {
return false; return false;
} }
return sub.is_announcement_only; return sub.stream_post_policy;
}; };
exports.all_topics_in_cache = function (sub) { exports.all_topics_in_cache = function (sub) {

View File

@ -272,6 +272,7 @@ exports.show_settings_for = function (node) {
const html = render_subscription_settings({ const html = render_subscription_settings({
sub: sub, sub: sub,
settings: exports.stream_settings(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); 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) { exports.bulk_set_stream_property = function (sub_data) {
return channel.post({ return channel.post({
url: '/json/users/me/subscriptions/properties', 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 sub = stream_data.get_sub_by_id(stream_id);
const privacy_setting = $('#stream_privacy_modal input[name=privacy]:checked').val(); 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 invite_only;
let history_public_to_subscribers; let history_public_to_subscribers;
@ -380,7 +380,7 @@ function change_stream_privacy(e) {
stream_name: sub.name, stream_name: sub.name,
// toggle the privacy setting // toggle the privacy setting
is_private: JSON.stringify(invite_only), 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), history_public_to_subscribers: JSON.stringify(history_public_to_subscribers),
}; };
@ -493,7 +493,8 @@ exports.initialize = function () {
const template_data = { const template_data = {
stream_id: stream_id, stream_id: stream_id,
stream_name: stream.name, 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_public: !stream.invite_only,
is_private: stream.invite_only && !stream.history_public_to_subscribers, is_private: stream.invite_only && !stream.history_public_to_subscribers,
is_private_with_public_history: stream.invite_only && 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': case 'wildcard_mentions_notify':
update_stream_setting(sub, value, property); update_stream_setting(sub, value, property);
break; break;
case 'is_announcement_only': case 'stream_post_policy':
subs.update_stream_announcement_only(sub, value); subs.update_stream_post_policy(sub, value);
break; break;
default: default:
blueslip.warn("Unexpected subscription property type", {property: property, 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 stream_settings = stream_edit.settings_for_sub(sub);
const html = render_subscription_type(sub); const html = render_subscription_type(sub);
if (stream_edit.is_sub_settings_active(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 // Update UI elements
stream_ui_updates.update_stream_privacy_type_icon(sub); 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_change_stream_privacy_settings(sub);
stream_ui_updates.update_settings_button_for_sub(sub); stream_ui_updates.update_settings_button_for_sub(sub);
stream_ui_updates.update_subscribers_count(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); stream_list.redraw_stream_privacy(sub);
}; };
exports.update_stream_announcement_only = function (sub, new_value) { exports.update_stream_post_policy = function (sub, new_value) {
stream_data.update_stream_announcement_only(sub, new_value); stream_data.update_stream_post_policy(sub, new_value);
stream_data.update_calculated_fields(sub); 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) { 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_name_length: page_params.stream_name_max_length,
max_description_length: page_params.stream_description_max_length, max_description_length: page_params.stream_description_max_length,
is_admin: page_params.is_admin, 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); 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>.'}} {{t 'These settings are explained in detail in the <a target="_blank" href="/help/stream-permissions">help center</a>.'}}
</div> </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"> <div id="announce-new-stream">
<label class="checkbox"> <label class="checkbox">

View File

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

View File

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

View File

@ -8,8 +8,10 @@
{{else}} {{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.' }} {{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}}
{{#if is_announcement_only}} {{#if (eq stream_post_policy stream_post_policy_values.admins.code)}}
{{t 'Only organization administrators can post.'}} {{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}} {{else}}
{{t 'All stream members can post.'}} {{t 'All stream members can post.'}}
{{/if}} {{/if}}

View File

@ -7,7 +7,7 @@ including:
* Stream [name](/help/rename-a-stream) and [description](/help/change-the-stream-description) * Stream [name](/help/rename-a-stream) and [description](/help/change-the-stream-description)
* Stream [permissions](/help/stream-permissions), including * Stream [permissions](/help/stream-permissions), including
[privacy](/help/change-the-privacy-of-a-stream) and [who can [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}` `PATCH {{ api_url }}/v1/streams/{stream_id}`

View File

@ -157,4 +157,4 @@
* [Change a stream's description](/help/change-the-stream-description) * [Change a stream's description](/help/change-the-stream-description)
* [Change the privacy of a stream](/help/change-the-privacy-of-a-stream) * [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) * [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 * Link to a code of conduct in your
[organization description](/help/create-your-organization-profile) [organization description](/help/create-your-organization-profile)
(displayed on the registration page). (displayed on the registration page).
* Create at least one * Create a [default stream](/help/set-default-streams-for-new-users)
[default stream](/help/set-default-streams-for-new-users) where for announcements where [only admins can
[only admins can post](/help/announcement-only-streams). post](/help/stream-sending-policy).
* Add a [waiting period](/help/restrict-permissions-of-new-members) before * Add a [waiting period](/help/restrict-permissions-of-new-members) before
new users can take disruptive actions. new users can take disruptive actions.
* [Restrict email visibility](/help/restrict-visibility-of-email-addresses) * [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) - [Creating streams](/help/configure-who-can-create-streams)
- [Adding users to streams](/help/configure-who-can-invite-to-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 ### 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 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. 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 * **Public**: Anyone other than guests can join, and anyone (other
history without joining. than guests) can view the complete message history without joining.
* **Private, shared history**: You must be added by a member of the stream. The * **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. 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 &#9726; &nbsp; If subscribed to the stream
&#10038; Configurable. Org admins and Members can, by default, post to &#10038; [Configurable](/help/stream-sending-policy). Org admins and
any public stream, and Guests can only post to public streams if they Members can, by default, post to any public stream, and Guests can
are subscribed. Additionally, streams can be configured to only allow only post to public streams if they are subscribed.
administrators to post.
### Private streams ### Private streams
@ -90,4 +88,5 @@ administrators to post.
&#9726; &nbsp; If subscribed to the stream &#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!} {!admin-only.md!}
By default, anyone who belongs to a stream can also send messages to the By default, anyone who belongs to a stream can also send messages to
stream. However, sometimes it's useful to have a stream (often a the stream. However, sometimes it's useful to have a stream (often a
[default stream](/help/set-default-streams-for-new-users)) where only [default stream](/help/set-default-streams-for-new-users)) where only
organization administrators can send messages. certain users can send messages.
### Restrict posting to organization administrators
{start_tabs} {start_tabs}
@ -18,7 +16,7 @@ organization administrators can send messages.
1. On the right, click **[Change]** next to the description of the stream 1. On the right, click **[Change]** next to the description of the stream
permissions. 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**. 1. Click **Save Changes**.

View File

@ -1824,7 +1824,7 @@ def create_stream_if_needed(realm: Realm,
stream_name: str, stream_name: str,
*, *,
invite_only: bool=False, 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, history_public_to_subscribers: Optional[bool]=None,
stream_description: str="") -> Tuple[Stream, bool]: stream_description: str="") -> Tuple[Stream, bool]:
@ -1838,7 +1838,7 @@ def create_stream_if_needed(realm: Realm,
name=stream_name, name=stream_name,
description=stream_description, description=stream_description,
invite_only=invite_only, invite_only=invite_only,
is_announcement_only=is_announcement_only, stream_post_policy=stream_post_policy,
history_public_to_subscribers=history_public_to_subscribers, history_public_to_subscribers=history_public_to_subscribers,
is_in_zephyr_realm=realm.is_zephyr_mirror_realm is_in_zephyr_realm=realm.is_zephyr_mirror_realm
) )
@ -1878,7 +1878,7 @@ def create_streams_if_needed(realm: Realm,
realm, realm,
stream_dict["name"], stream_dict["name"],
invite_only=stream_dict.get("invite_only", False), 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"), history_public_to_subscribers=stream_dict.get("history_public_to_subscribers"),
stream_description=stream_dict.get("description", "") 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 # Our caller is responsible for making sure that `stream` actually
# matches the realm of the sender. # matches the realm of the sender.
if stream.is_announcement_only: # 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): if sender.is_realm_admin or is_cross_realm_bot_email(sender.delivery_email):
pass pass
elif sender.is_bot and (sender.bot_owner is not None and elif sender.is_bot and (sender.bot_owner is not None and
sender.bot_owner.is_realm_admin): sender.bot_owner.is_realm_admin):
pass pass
else: elif stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS:
raise JsonableError(_("Only organization administrators can send to this stream.")) 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): if not (stream.invite_only or sender.is_guest):
# This is a public stream and sender is not a guest user # 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.is_web_public = is_web_public
stream.save(update_fields=['is_web_public']) stream.save(update_fields=['is_web_public'])
def do_change_stream_announcement_only(stream: Stream, is_announcement_only: bool) -> None: def do_change_stream_post_policy(stream: Stream, stream_post_policy: int) -> None:
stream.is_announcement_only = is_announcement_only stream.stream_post_policy = stream_post_policy
stream.save(update_fields=['is_announcement_only']) 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( event = dict(
op="update", op="update",
type="stream", type="stream",
property="is_announcement_only", property="is_announcement_only",
value=is_announcement_only, value=stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS,
stream_id=stream.id, stream_id=stream.id,
name=stream.name, name=stream.name,
) )
@ -4818,6 +4838,11 @@ def gather_subscriptions_helper(user_profile: UserProfile,
# Backwards-compatibility for clients that haven't been # Backwards-compatibility for clients that haven't been
# updated for the in_home_view => is_muted API migration. # updated for the in_home_view => is_muted API migration.
stream_dict['in_home_view'] = not stream_dict['is_muted'] 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. # Add a few computed fields not directly from the data models.
stream_dict['is_old_stream'] = is_old_stream(stream["date_created"]) 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['is_old_stream'] = is_old_stream(stream["date_created"])
stream_dict['stream_weekly_traffic'] = get_average_weekly_stream_traffic( stream_dict['stream_weekly_traffic'] = get_average_weekly_stream_traffic(
stream["id"], stream["date_created"], recent_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: if is_public or user_profile.is_realm_admin:
subscribers = subscriber_map[stream["id"]] subscribers = subscriber_map[stream["id"]]

View File

@ -113,7 +113,8 @@ def bulk_create_streams(realm: Realm,
description=options["description"], description=options["description"],
rendered_description=render_stream_description(options["description"]), rendered_description=render_stream_description(options["description"]),
invite_only=options.get("invite_only", False), 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"], history_public_to_subscribers=options["history_public_to_subscribers"],
is_web_public=options.get("is_web_public", False), is_web_public=options.get("is_web_public", False),
is_in_zephyr_realm=realm.is_zephyr_mirror_realm, 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['subscribers'] = []
stream_data['stream_weekly_traffic'] = None stream_data['stream_weekly_traffic'] = None
stream_data['is_old_stream'] = False 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) # Add stream to never_subscribed (if not invite_only)
state['never_subscribed'].append(stream_data) state['never_subscribed'].append(stream_data)
state['streams'].append(stream) 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_name = stream_dict["name"]
stream = existing_stream_map.get(stream_name.lower()) stream = existing_stream_map.get(stream_name.lower())
if stream is None: 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 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) missing_stream_dicts.append(stream_dict)
else: else:
existing_streams.append(stream) existing_streams.append(stream)

View File

@ -292,12 +292,12 @@ class ZulipTestCase(TestCase):
def notification_bot(self) -> UserProfile: def notification_bot(self) -> UserProfile:
return get_system_bot(settings.NOTIFICATION_BOT) 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]: assert_json_error_msg: str=None, **extras: Any) -> Optional[UserProfile]:
self.login(user_profile.delivery_email) self.login(user_profile.delivery_email)
bot_info = { bot_info = {
'short_name': short_name, 'short_name': short_name,
'full_name': 'Foo Bot', 'full_name': full_name,
} }
bot_info.update(extras) bot_info.update(extras)
result = self.client_post("/json/bots", bot_info) 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 # Whether this stream's content should be published by the web-public archive features
is_web_public = models.BooleanField(default=False) # type: bool is_web_public = models.BooleanField(default=False) # type: bool
# Whether only organization administrators can send messages to this stream STREAM_POST_POLICY_EVERYONE = 1
is_announcement_only = models.BooleanField(default=False) # type: bool 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 # 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 # users. We may try to generalize this concept later, but for now
@ -1434,7 +1444,7 @@ class Stream(models.Model):
"rendered_description", "rendered_description",
"invite_only", "invite_only",
"is_web_public", "is_web_public",
"is_announcement_only", "stream_post_policy",
"history_public_to_subscribers", "history_public_to_subscribers",
"first_message_id", "first_message_id",
] ]
@ -1447,6 +1457,7 @@ class Stream(models.Model):
result['stream_id'] = self.id result['stream_id'] = self.id
continue continue
result[field_name] = getattr(self, field_name) result[field_name] = getattr(self, field_name)
result['is_announcement_only'] = self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS
return result return result
post_save.connect(flush_stream, sender=Stream) 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 # Update the stream by a given ID
request = { request = {
'stream_id': stream_id, 'stream_id': stream_id,
'is_announcement_only': True, 'stream_post_policy': 2,
'is_private': True, 'is_private': True,
} }

View File

@ -1504,14 +1504,13 @@ paths:
type: boolean type: boolean
default: None default: None
example: false example: false
- name: is_announcement_only - name: stream_post_policy
in: query in: query
description: A boolean indicating if the stream is an announcements only stream. description: Indicates which users can post messages to the stream.
Only organization admins can post to announcements only streams.
schema: schema:
type: boolean type: integer
default: false default: 1
example: false example: 1
- name: announce - name: announce
in: query in: query
description: If `announce` is `True` and one of the streams specified description: If `announce` is `True` and one of the streams specified
@ -2624,6 +2623,13 @@ paths:
type: boolean type: boolean
example: true example: true
required: false 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 - name: history_public_to_subscribers
in: query in: query
description: The new state for the history_public_to_subscribers. 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_realm_domain,
do_change_stream_description, do_change_stream_description,
do_change_stream_invite_only, do_change_stream_invite_only,
do_change_stream_announcement_only, do_change_stream_post_policy,
do_change_subscription_property, do_change_subscription_property,
do_change_user_delivery_email, do_change_user_delivery_email,
do_create_user, do_create_user,
@ -134,7 +134,7 @@ from zerver.lib.topic_mutes import (
) )
from zerver.lib.validator import ( from zerver.lib.validator import (
check_bool, check_dict, check_dict_only, check_float, check_int, check_list, check_string, 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 from zerver.lib.users import get_api_key
@ -1438,6 +1438,7 @@ class EventsRegisterTest(ZulipTestCase):
('invite_only', check_bool), ('invite_only', check_bool),
('is_web_public', check_bool), ('is_web_public', check_bool),
('is_announcement_only', check_bool), ('is_announcement_only', check_bool),
('stream_post_policy', check_int_in(Stream.STREAM_POST_POLICY_TYPES)),
('name', check_string), ('name', check_string),
('stream_id', check_int), ('stream_id', check_int),
('first_message_id', check_none_or(check_int)), ('first_message_id', check_none_or(check_int)),
@ -2508,6 +2509,7 @@ class EventsRegisterTest(ZulipTestCase):
('invite_only', check_bool), ('invite_only', check_bool),
('is_web_public', check_bool), ('is_web_public', check_bool),
('is_announcement_only', check_bool), ('is_announcement_only', check_bool),
('stream_post_policy', check_int_in(Stream.STREAM_POST_POLICY_TYPES)),
('is_muted', check_bool), ('is_muted', check_bool),
('in_home_view', check_bool), ('in_home_view', check_bool),
('name', check_string), ('name', check_string),
@ -2584,13 +2586,13 @@ class EventsRegisterTest(ZulipTestCase):
('value', check_bool), ('value', check_bool),
('history_public_to_subscribers', 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')), ('type', equals('stream')),
('op', equals('update')), ('op', equals('update')),
('property', equals('is_announcement_only')), ('property', equals('stream_post_policy')),
('stream_id', check_int), ('stream_id', check_int),
('name', check_string), ('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 # 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]) error = stream_update_invite_only_schema_checker('events[0]', events[0])
self.assert_on_error(error) self.assert_on_error(error)
# Update stream is_announcement_only property # Update stream stream_post_policy property
action = lambda: do_change_stream_announcement_only(stream, True) action = lambda: do_change_stream_post_policy(stream, Stream.STREAM_POST_POLICY_ADMINS)
events = self.do_test(action, events = self.do_test(action,
include_subscribers=include_subscribers) include_subscribers=include_subscribers, num_events=2)
error = stream_update_is_announcement_only_schema_checker('events[0]', events[0]) error = stream_update_stream_post_policy_schema_checker('events[0]', events[0])
self.assert_on_error(error) self.assert_on_error(error)
# 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

View File

@ -1424,6 +1424,15 @@ class SewMessageAndReactionTest(ZulipTestCase):
class MessagePOSTTest(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: def test_message_to_self(self) -> None:
""" """
Sending a message to a stream to which you are subscribed is 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() sent_message = self.get_last_message()
self.assertEqual(sent_message.content, "Stream message by ID.") 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 Sending messages to streams which only the admins can create and post to.
successful.
""" """
user_profile = self.example_user("iago") admin_profile = self.example_user("iago")
self.login(user_profile.email) self.login(admin_profile.email)
stream_name = "Verona" stream_name = "Verona"
stream = get_stream(stream_name, user_profile.realm) stream = get_stream(stream_name, admin_profile.realm)
stream.is_announcement_only = True stream.stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS
stream.save() 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( admin_owned_bot = self.create_test_bot(
short_name='whatever', short_name='whatever1',
user_profile=user_profile, full_name='whatever1',
user_profile=admin_profile,
) )
result = self.api_post(admin_owned_bot.email, self._send_and_verify_message(admin_owned_bot.email, stream_name)
"/api/v1/messages", {"type": "stream",
"to": stream_name, non_admin_profile = self.example_user("hamlet")
"client": "test suite", self.login(non_admin_profile.email)
"content": "Test message",
"topic": "Test topic"}) # Non admins and their owned bots cannot send to STREAM_POST_POLICY_ADMINS streams
self.assert_json_success(result) 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) # Cross realm bots should be allowed (through internal_send_message)
notification_bot = get_system_bot("notification-bot@zulip.com") notification_bot = get_system_bot("notification-bot@zulip.com")
@ -1526,54 +1551,71 @@ class MessagePOSTTest(ZulipTestCase):
'Test topic', 'Test message by notification bot') 'Test topic', 'Test message by notification bot')
self.assertEqual(self.get_last_message().content, '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 Sending messages to streams which new members cannot create and post to.
admin fails.
""" """
user_profile = self.example_user("hamlet") admin_profile = self.example_user("iago")
self.login(user_profile.email) 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_name = "Verona"
stream = get_stream(stream_name, user_profile.realm) stream = get_stream(stream_name, admin_profile.realm)
stream.is_announcement_only = True stream.stream_post_policy = Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS
stream.save() 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 # Admins and their owned bots can send to STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS streams,
non_admin_owned_bot = self.create_test_bot( # even if the admin is a new user
short_name='whatever', self._send_and_verify_message(admin_profile.email, stream_name)
user_profile=user_profile, 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, self._send_and_verify_message(admin_owned_bot.email, stream_name)
"/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.")
# 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( bot_without_owner = do_create_user(
email='free-bot@zulip.testserver', email='free-bot@zulip.testserver',
password='', password='',
realm=user_profile.realm, realm=non_admin_profile.realm,
full_name='freebot', full_name='freebot',
short_name='freebot', short_name='freebot',
bot_type=UserProfile.DEFAULT_BOT, bot_type=UserProfile.DEFAULT_BOT,
) )
result = self.api_post(bot_without_owner.email, self._send_and_verify_message(bot_without_owner.email, stream_name,
"/api/v1/messages", {"type": "stream", "New members cannot send to this stream.")
"to": stream_name,
"client": "test suite", # Cross realm bots should be allowed (through internal_send_message)
"content": "Test message", notification_bot = get_system_bot("notification-bot@zulip.com")
"topic": "Test topic"}) internal_send_stream_message(stream.realm, notification_bot, stream,
self.assert_json_error(result, "Only organization administrators can send to this 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: def test_api_message_with_default_to(self) -> None:
""" """

View File

@ -142,7 +142,7 @@ class TestCreateStreams(ZulipTestCase):
[{"name": stream_name, [{"name": stream_name,
"description": stream_description, "description": stream_description,
"invite_only": True, "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)]) for (stream_name, stream_description) in zip(stream_names, stream_descriptions)])
self.assertEqual(len(new_streams), 3) self.assertEqual(len(new_streams), 3)
@ -154,7 +154,7 @@ class TestCreateStreams(ZulipTestCase):
self.assertEqual(actual_stream_descriptions, set(stream_descriptions)) self.assertEqual(actual_stream_descriptions, set(stream_descriptions))
for stream in new_streams: for stream in new_streams:
self.assertTrue(stream.invite_only) 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( new_streams, existing_streams = create_streams_if_needed(
realm, realm,
@ -264,7 +264,7 @@ class TestCreateStreams(ZulipTestCase):
"invite_only": 'false', "invite_only": 'false',
"announce": 'true', "announce": 'true',
"principals": '["iago@zulip.com", "AARON@zulip.com", "cordelia@zulip.com", "hamlet@zulip.com"]', "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) response = self.client_post("/json/users/me/subscriptions", data)
@ -780,7 +780,7 @@ class StreamAdminTest(ZulipTestCase):
{'description': ujson.dumps('Test description')}) {'description': ujson.dumps('Test description')})
self.assert_json_error(result, 'Must be an organization administrator') 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') user_profile = self.example_user('hamlet')
self.login(user_profile.email) self.login(user_profile.email)
@ -792,9 +792,9 @@ class StreamAdminTest(ZulipTestCase):
{'is_announcement_only': ujson.dumps(True)}) {'is_announcement_only': ujson.dumps(True)})
self.assert_json_success(result) self.assert_json_success(result)
stream = get_stream('stream_name1', user_profile.realm) 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') user_profile = self.example_user('hamlet')
self.login(user_profile.email) self.login(user_profile.email)
@ -803,7 +803,17 @@ class StreamAdminTest(ZulipTestCase):
stream_id = get_stream('stream_name1', user_profile.realm).id stream_id = get_stream('stream_name1', user_profile.realm).id
result = self.client_patch('/json/streams/%d' % (stream_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') self.assert_json_error(result, 'Must be an organization administrator')
def set_up_stream_for_deletion(self, stream_name: str, invite_only: bool=False, 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['event']['op'], 'add')
self.assertEqual(add_event['users'], [self.example_user("iago").id]) 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 Members can subscribe to streams where only admins can post
but not create those streams, only realm admins can but not create those streams, only realm admins can
@ -2581,7 +2591,7 @@ class SubscriptionAPITest(ZulipTestCase):
streams_raw = [{ streams_raw = [{
'name': 'new_stream', 'name': 'new_stream',
'is_announcement_only': True, 'stream_post_policy': Stream.STREAM_POST_POLICY_ADMINS,
}] }]
with self.assertRaisesRegex( with self.assertRaisesRegex(
JsonableError, "User cannot create a stream with these settings."): 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[0], 0)
self.assert_length(result[1], 1) self.assert_length(result[1], 1)
self.assertEqual(result[1][0].name, 'new_stream') 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: def test_guest_user_subscribe(self) -> None:
"""Guest users cannot subscribe themselves to anything""" """Guest users cannot subscribe themselves to anything"""
@ -2611,7 +2670,7 @@ class SubscriptionAPITest(ZulipTestCase):
'invite_only': False, 'invite_only': False,
'history_public_to_subscribers': None, 'history_public_to_subscribers': None,
'name': 'new_stream', 'name': 'new_stream',
'is_announcement_only': False 'stream_post_policy': Stream.STREAM_POST_POLICY_EVERYONE
}] }]
with self.assertRaisesRegex(JsonableError, "User cannot create streams."): 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, \ 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_deactivate_stream, do_change_stream_invite_only, do_add_default_stream, \
do_change_stream_description, do_get_streams, \ 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_create_default_stream_group, do_add_streams_to_default_stream_group, \
do_remove_streams_from_default_stream_group, do_remove_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_default_stream_group_description, do_change_default_stream_group_name
do_change_stream_announcement_only, \
do_delete_messages
from zerver.lib.response import json_success, json_error from zerver.lib.response import json_success, json_error
from zerver.lib.streams import access_stream_by_id, access_stream_by_name, \ from zerver.lib.streams import access_stream_by_id, access_stream_by_name, \
check_stream_name, check_stream_name_available, filter_stream_authorization, \ 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 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.topic import get_topic_history_for_stream, messages_for_topic
from zerver.lib.validator import check_string, check_int, check_list, check_dict, \ 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, \ from zerver.models import UserProfile, Stream, Realm, UserMessage, \
get_system_bot, get_active_user get_system_bot, get_active_user
@ -150,6 +149,8 @@ def update_stream_backend(
Stream.MAX_DESCRIPTION_LENGTH), default=None), Stream.MAX_DESCRIPTION_LENGTH), default=None),
is_private: Optional[bool]=REQ(validator=check_bool, default=None), is_private: Optional[bool]=REQ(validator=check_bool, default=None),
is_announcement_only: 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), history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None),
new_name: Optional[str]=REQ(validator=check_string, default=None), new_name: Optional[str]=REQ(validator=check_string, default=None),
) -> HttpResponse: ) -> HttpResponse:
@ -171,7 +172,15 @@ def update_stream_backend(
check_stream_name_available(user_profile.realm, new_name) check_stream_name_available(user_profile.realm, new_name)
do_rename_stream(stream, new_name, user_profile) do_rename_stream(stream, new_name, user_profile)
if is_announcement_only is not None: 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 # But we require even realm administrators to be actually
# subscribed to make a private stream public. # subscribed to make a private stream public.
@ -296,7 +305,8 @@ def add_subscriptions_backend(
]) ])
)), )),
invite_only: bool=REQ(validator=check_bool, default=False), 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), history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None),
announce: bool=REQ(validator=check_bool, default=False), announce: bool=REQ(validator=check_bool, default=False),
principals: List[str]=REQ(validator=check_list(check_string), default=[]), principals: List[str]=REQ(validator=check_list(check_string), default=[]),
@ -319,7 +329,7 @@ def add_subscriptions_backend(
# Strip the stream name here. # Strip the stream name here.
stream_dict_copy['name'] = stream_dict_copy['name'].strip() stream_dict_copy['name'] = stream_dict_copy['name'].strip()
stream_dict_copy["invite_only"] = invite_only 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_dict_copy["history_public_to_subscribers"] = history_public_to_subscribers
stream_dicts.append(stream_dict_copy) stream_dicts.append(stream_dict_copy)

View File

@ -526,7 +526,8 @@ class Command(BaseCommand):
zulip_stream_dict = { zulip_stream_dict = {
"devel": {"description": "For developing"}, "devel": {"description": "For developing"},
"all": {"description": "For **everything**"}, "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"}, "design": {"description": "For design"},
"support": {"description": "For support"}, "support": {"description": "For support"},
"social": {"description": "For socializing"}, "social": {"description": "For socializing"},