From aa5c02231e6b02ad98d31ac7887b38d7dcc06e6e Mon Sep 17 00:00:00 2001 From: sahil839 Date: Mon, 17 Feb 2020 02:50:18 +0530 Subject: [PATCH] stream settings: Stream_post_policy field for restricting reactions. Option to stream_post_policy is added for restricting posting and reacting to admins only. This restricts reaction of emojis to admins only. Fixes #12835 --- frontend_tests/node_tests/stream_data.js | 37 ++++++++++++++++++++-- frontend_tests/node_tests/stream_events.js | 2 ++ static/js/click_handlers.js | 11 +++++-- static/js/compose.js | 3 +- static/js/emoji_picker.js | 12 +++++-- static/js/message_list_view.js | 2 ++ static/js/popovers.js | 12 ++++++- static/js/stream_data.js | 13 ++++++++ static/js/stream_events.js | 1 + static/js/stream_ui_updates.js | 9 +++++- static/styles/reactions.scss | 20 +++++++++--- static/styles/zulip.scss | 2 +- static/templates/message_body.hbs | 2 +- static/templates/message_controls.hbs | 5 +-- static/templates/message_reaction.hbs | 3 +- static/templates/message_reactions.hbs | 2 +- static/templates/subscription_type.hbs | 2 ++ zerver/lib/actions.py | 11 +++++-- zerver/lib/streams.py | 9 ++++-- zerver/models.py | 5 ++- zerver/tests/test_reactions.py | 29 ++++++++++++++++- zerver/tests/test_subs.py | 25 ++++++++++++++- zerver/views/reactions.py | 9 +++++- 23 files changed, 197 insertions(+), 29 deletions(-) diff --git a/frontend_tests/node_tests/stream_data.js b/frontend_tests/node_tests/stream_data.js index 043ace3007694d..6404220695ec65 100644 --- a/frontend_tests/node_tests/stream_data.js +++ b/frontend_tests/node_tests/stream_data.js @@ -494,8 +494,8 @@ 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].stream_post_policy === - stream_data.stream_post_policy_values.admins.code, true); + assert.equal(sub_rows[0].stream_post_policy, + stream_data.stream_post_policy_values.admins.code); const sub = stream_data.get_sub('a'); stream_data.update_stream_privacy(sub, { @@ -1073,3 +1073,36 @@ run_test('all_topics_in_cache', () => { sub.first_message_id = 2; assert.equal(stream_data.all_topics_in_cache(sub), true); }); + +run_test('get_restrict_emoji_reaction', () => { + const general = { + name: 'general', + stream_id: 1, + stream_post_policy: stream_data.stream_post_policy_values.everyone.code, + }; + + const test = { + name: 'test', + stream_id: 1, + stream_post_policy: stream_data.stream_post_policy_values.admins_can_post_and_react.code, + }; + + stream_data.add_sub(general); + stream_data.add_sub(test); + + page_params.is_admin = false; + + let restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('general'); + assert.equal(restrict_emoji_reaction, false); + + restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('test'); + assert.equal(restrict_emoji_reaction, true); + + page_params.is_admin = true; + + restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('general'); + assert.equal(restrict_emoji_reaction, false); + + restrict_emoji_reaction = stream_data.get_restrict_emoji_reaction('test'); + assert.equal(restrict_emoji_reaction, false); +}); diff --git a/frontend_tests/node_tests/stream_events.js b/frontend_tests/node_tests/stream_events.js index 61d22f8c9f8ed9..14a0208a5a89dc 100644 --- a/frontend_tests/node_tests/stream_events.js +++ b/frontend_tests/node_tests/stream_events.js @@ -2,10 +2,12 @@ const noop = function () {}; const return_true = function () { return true; }; set_global('$', global.make_zjquery()); set_global('document', 'document-stub'); + const _settings_notifications = { update_page: () => {}, }; set_global('settings_notifications', _settings_notifications); +set_global('current_msg_list', {rerender: noop}); zrequire('people'); zrequire('stream_data'); diff --git a/static/js/click_handlers.js b/static/js/click_handlers.js index 31973b07b8a0ed..1e214209c286cb 100644 --- a/static/js/click_handlers.js +++ b/static/js/click_handlers.js @@ -140,7 +140,10 @@ exports.initialize = function () { e.stopPropagation(); const local_id = $(this).attr('data-reaction-id'); const message_id = rows.get_message_id(this); - reactions.process_reaction_click(message_id, local_id); + const message = current_msg_list.get(message_id); + if (!message.is_stream || !stream_data.get_restrict_emoji_reaction(message.stream)) { + reactions.process_reaction_click(message_id, local_id); + } $(".tooltip").remove(); }); @@ -179,7 +182,10 @@ exports.initialize = function () { const local_id = elem.attr('data-reaction-id'); const message_id = rows.get_message_id(e.currentTarget); const title = reactions.get_reaction_title_data(message_id, local_id); - + const message = current_msg_list.get(message_id); + if (message.is_stream && stream_data.get_restrict_emoji_reaction(message.stream)) { + $(this).closest(".message_reaction").find(".disable-reaction-button").show(); + } elem.tooltip({ title: title, trigger: 'hover', @@ -195,6 +201,7 @@ exports.initialize = function () { $('#main_div').on('mouseleave', '.message_reaction', function (e) { e.stopPropagation(); $(e.currentTarget).tooltip('destroy'); + $(this).closest(".message_reaction").find(".disable-reaction-button").hide(); }); // DESTROY PERSISTING TOOLTIPS ON HOVER diff --git a/static/js/compose.js b/static/js/compose.js index 8402b398766b06..e3aa1531929136 100644 --- a/static/js/compose.js +++ b/static/js/compose.js @@ -514,7 +514,8 @@ function validate_stream_message_post_policy(stream_name) { 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) { + if (stream_post_policy === stream_post_permission_type.admins.code || + stream_post_policy === stream_post_permission_type.admins_can_post_and_react.code) { compose_error(i18n.t("Only organization admins are allowed to post to this stream.")); return false; } diff --git a/static/js/emoji_picker.js b/static/js/emoji_picker.js index 373e662858d72f..4979565fa0bf15 100644 --- a/static/js/emoji_picker.js +++ b/static/js/emoji_picker.js @@ -659,14 +659,21 @@ exports.register_click_handlers = function () { $("#main_div").on("click", ".reaction_button", function (e) { e.stopPropagation(); - const message_id = rows.get_message_id(this); - exports.toggle_emoji_popover(this, message_id); + const message = current_msg_list.get(message_id); + if (!message.is_stream || !stream_data.get_restrict_emoji_reaction(message.stream)) { + exports.toggle_emoji_popover(this, message_id); + } }); $("#main_div").on("mouseenter", ".reaction_button", function (e) { e.stopPropagation(); + const message_id = rows.get_message_id(this); + const message = current_msg_list.get(message_id); + if (message.is_stream && stream_data.get_restrict_emoji_reaction(message.stream)) { + $(this).find(".disable-emoji-icon").show(); + } const elem = $(e.currentTarget); const title = i18n.t("Add emoji reaction"); elem.tooltip({ @@ -681,6 +688,7 @@ exports.register_click_handlers = function () { $('#main_div').on('mouseleave', '.reaction_button', function (e) { e.stopPropagation(); + $(this).find(".disable-emoji-icon").hide(); $(e.currentTarget).tooltip('hide'); }); diff --git a/static/js/message_list_view.js b/static/js/message_list_view.js index 2d6acf626301f2..e7cb9167d4d713 100644 --- a/static/js/message_list_view.js +++ b/static/js/message_list_view.js @@ -352,6 +352,8 @@ MessageListView.prototype = { if (message_container.msg.stream) { message_container.background_color = stream_data.get_color(message_container.msg.stream); + message_container.restrict_emoji_reaction = + stream_data.get_restrict_emoji_reaction(message_container.msg.stream); } message_container.contains_mention = message_container.msg.mentioned; diff --git a/static/js/popovers.js b/static/js/popovers.js index 56f381d4e0013b..b8fd2af07fbd4d 100644 --- a/static/js/popovers.js +++ b/static/js/popovers.js @@ -115,6 +115,16 @@ function calculate_info_popover_placement(size, elt) { } } +function show_add_reaction_option(message) { + if (!message.sent_by_me) { + return false; + } + if (message.stream && stream_data.get_restrict_emoji_reaction(message.stream)) { + return false; + } + return true; +} + function get_custom_profile_field_data(user, field, field_types, dateFormat) { const field_value = people.get_custom_profile_data(user.user_id, field.id); const field_type = field.type; @@ -485,7 +495,7 @@ exports.toggle_actions_popover = function (element, id) { can_unmute_topic: can_unmute_topic, should_display_collapse: should_display_collapse, should_display_uncollapse: should_display_uncollapse, - should_display_add_reaction_option: message.sent_by_me, + should_display_add_reaction_option: show_add_reaction_option(message), should_display_edit_history_option: should_display_edit_history_option, conversation_time_uri: conversation_time_uri, narrowed: narrow_state.active(), diff --git a/static/js/stream_data.js b/static/js/stream_data.js index fe20641eefe0af..b1b661934e0f48 100644 --- a/static/js/stream_data.js +++ b/static/js/stream_data.js @@ -103,6 +103,10 @@ exports.stream_post_policy_values = { code: 3, description: i18n.t("Only organization full members can post"), }, + admins_can_post_and_react: { + code: 4, + description: i18n.t("Only organization administrators can post and react"), + }, }; exports.clear_subscriptions = function () { @@ -541,6 +545,15 @@ exports.get_stream_post_policy = function (stream_name) { return sub.stream_post_policy; }; +exports.get_restrict_emoji_reaction = function (stream_name) { + const stream_post_policy = exports.get_stream_post_policy(stream_name); + if (stream_post_policy === exports.stream_post_policy_values.admins_can_post_and_react.code + && !page_params.is_admin) { + return true; + } + return false; +}; + exports.all_topics_in_cache = function (sub) { // Checks whether this browser's cache of contiguous messages // (used to locally render narrows) in message_list.all has all diff --git a/static/js/stream_events.js b/static/js/stream_events.js index 4b8a7a86938ce3..ee461f9b9c2d69 100644 --- a/static/js/stream_events.js +++ b/static/js/stream_events.js @@ -54,6 +54,7 @@ exports.update_property = function (stream_id, property, value, other_values) { break; case 'stream_post_policy': subs.update_stream_post_policy(sub, value); + current_msg_list.rerender(); break; default: blueslip.warn("Unexpected subscription property type", {property: property, diff --git a/static/js/stream_ui_updates.js b/static/js/stream_ui_updates.js index 67ec718a7e5d21..af25307b88d77c 100644 --- a/static/js/stream_ui_updates.js +++ b/static/js/stream_ui_updates.js @@ -143,7 +143,14 @@ exports.update_stream_privacy_type_icon = 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); + const template_data = { + invite_only: sub.invite_only, + history_public_to_subscribers: sub.history_public_to_subscribers, + is_web_public: sub.is_web_public, + stream_post_policy: sub.stream_post_policy, + stream_post_policy_values: stream_data.stream_post_policy_values, + }; + const html = render_subscription_type(template_data); if (stream_edit.is_sub_settings_active(sub)) { stream_settings.find('.subscription-type-text').expectOne().html(html); } diff --git a/static/styles/reactions.scss b/static/styles/reactions.scss index 9755f6dbfe7f2f..9f43472a48e31a 100644 --- a/static/styles/reactions.scss +++ b/static/styles/reactions.scss @@ -13,12 +13,13 @@ background-color: hsl(0, 0%, 100%); border: 1px solid hsl(194, 37%, 84%); border-radius: 4px; + position: relative; &.reacted { background-color: hsl(195, 50%, 95%); } - &:hover { + &:not(.disabled):hover { border: 1px solid hsl(200, 100%, 40%); } @@ -64,7 +65,7 @@ color: hsl(0, 0%, 33%); } - &:hover .message_reaction + .reaction_button { + &:not(.disabled):hover .message_reaction + .reaction_button { visibility: visible; pointer-events: all; background-color: hsl(0, 0%, 98%); @@ -76,7 +77,7 @@ margin-right: 3px; } - &:hover i { + &:not(.disabled):hover i { color: hsl(200, 100%, 40%); } @@ -84,7 +85,7 @@ display: none; } - &:hover { + &:not(.disabled):hover { border: 1px solid hsl(200, 100%, 40%); background-color: hsl(195, 50%, 95%); cursor: pointer; @@ -296,3 +297,14 @@ .typeahead .emoji { top: 2px; } + +.disable-emoji-icon, +.disable-reaction-button { + display: none; + position: absolute; + width: 100%; + height: 2px; + background-color: hsl(0, 100%, 0%); + top: 8px; + left: 0px; +} diff --git a/static/styles/zulip.scss b/static/styles/zulip.scss index 3b7977ef544384..299b5a51949d3e 100644 --- a/static/styles/zulip.scss +++ b/static/styles/zulip.scss @@ -651,7 +651,7 @@ td.pointer { display: inline-block; position: relative; color: hsl(0, 0%, 73%); - &:hover { + &:not(.disabled):hover { color: hsl(200, 100%, 40%); } } diff --git a/static/templates/message_body.hbs b/static/templates/message_body.hbs index d170205c94c6c9..e6d0995d1206df 100644 --- a/static/templates/message_body.hbs +++ b/static/templates/message_body.hbs @@ -48,4 +48,4 @@
{{t "[More...]" }}
{{t "[Condense message]" }}
-
{{> message_reactions }}
+
{{> message_reactions}}
diff --git a/static/templates/message_controls.hbs b/static/templates/message_controls.hbs index d52e502b8fae2e..cb49b51ba58832 100644 --- a/static/templates/message_controls.hbs +++ b/static/templates/message_controls.hbs @@ -4,8 +4,9 @@ {{/if}} {{#unless msg/sent_by_me}} -
- +
+ +
{{/unless}} diff --git a/static/templates/message_reaction.hbs b/static/templates/message_reaction.hbs index 6fadb7d92bcdc6..bedf8f811abc2d 100644 --- a/static/templates/message_reaction.hbs +++ b/static/templates/message_reaction.hbs @@ -1,4 +1,4 @@ -
+
{{#if this.emoji_alt_code}}
 :{{this.emoji_name}}:
{{else}} @@ -8,5 +8,6 @@
{{/if}} {{/if}} +
{{this.count}}
diff --git a/static/templates/message_reactions.hbs b/static/templates/message_reactions.hbs index 995516fd80057a..d31d63666249cd 100644 --- a/static/templates/message_reactions.hbs +++ b/static/templates/message_reactions.hbs @@ -1,5 +1,5 @@ {{#each this/msg/message_reactions}} -{{> message_reaction}} +{{> message_reaction restrict_emoji_reaction = ../restrict_emoji_reaction}} {{/each}}
diff --git a/static/templates/subscription_type.hbs b/static/templates/subscription_type.hbs index a453813a7b24f3..8b395e2b492bc3 100644 --- a/static/templates/subscription_type.hbs +++ b/static/templates/subscription_type.hbs @@ -10,6 +10,8 @@ {{/if}} {{#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.admins_can_post_and_react.code)}} +{{t 'Only orgainzation administrators can post and react'}} {{else if (eq stream_post_policy stream_post_policy_values.non_new_members.code)}} {{t 'Only organization full members can post.'}} {{else}} diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index 846e4333d2be0f..b040c810422b39 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -3522,11 +3522,14 @@ def do_change_stream_post_policy(stream: Stream, stream_post_policy: int) -> Non # is_announcement_only property in early 2020, but we send a # duplicate event for legacy mobile clients that might want the # data. + is_announcement_only_value = (stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS or + stream.stream_post_policy == + Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT) event = dict( op="update", type="stream", property="is_announcement_only", - value=stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS, + value=is_announcement_only_value, stream_id=stream.id, name=stream.name, ) @@ -4783,7 +4786,8 @@ def gather_subscriptions_helper(user_profile: UserProfile, # updated for the is_announcement_only -> stream_post_policy # migration. stream_dict['is_announcement_only'] = \ - stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS or \ + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT # Add a few computed fields not directly from the data models. stream_dict['is_old_stream'] = is_old_stream(stream["date_created"]) @@ -4834,7 +4838,8 @@ def gather_subscriptions_helper(user_profile: UserProfile, 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 + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS or \ + stream['stream_post_policy'] == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT if is_public or user_profile.is_realm_admin: subscribers = subscriber_map[stream["id"]] diff --git a/zerver/lib/streams.py b/zerver/lib/streams.py index a74a8e44b6dae6..78366235e4c943 100644 --- a/zerver/lib/streams.py +++ b/zerver/lib/streams.py @@ -133,7 +133,8 @@ def access_stream_for_send_message(sender: UserProfile, 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: + elif (stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS or + stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT): 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 @@ -422,8 +423,10 @@ def list_to_streams(streams_raw: Iterable[Mapping[str, Any]], stream = existing_stream_map.get(stream_name.lower()) if stream is None: # 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): + if (((stream_dict.get("stream_post_policy", False) == + Stream.STREAM_POST_POLICY_ADMINS) or (stream_dict.get("stream_post_policy", False) == + Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT)) + 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. diff --git a/zerver/models.py b/zerver/models.py index 24376f00c823bb..8998b44cbcc6b7 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -1370,6 +1370,7 @@ class Stream(models.Model): STREAM_POST_POLICY_EVERYONE = 1 STREAM_POST_POLICY_ADMINS = 2 STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS = 3 + STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT = 4 # TODO: Implement policy to restrict posting to a user group or admins. # Who in the organization has permission to send messages to this stream. @@ -1378,6 +1379,7 @@ class Stream(models.Model): STREAM_POST_POLICY_EVERYONE, STREAM_POST_POLICY_ADMINS, STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS, + STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT, ] # The unique thing about Zephyr public streams is that we never list their @@ -1456,7 +1458,8 @@ def to_dict(self) -> Dict[str, Any]: 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 + result['is_announcement_only'] = self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS or \ + self.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT return result post_save.connect(flush_stream, sender=Stream) diff --git a/zerver/tests/test_reactions.py b/zerver/tests/test_reactions.py index 562bc6feb03b3d..e3520aab9e557a 100644 --- a/zerver/tests/test_reactions.py +++ b/zerver/tests/test_reactions.py @@ -7,7 +7,7 @@ from zerver.lib.request import JsonableError from zerver.lib.test_helpers import tornado_redirected_to_list from zerver.lib.test_classes import ZulipTestCase -from zerver.models import get_realm, Message, Reaction, RealmEmoji, UserMessage +from zerver.models import get_realm, Message, Reaction, RealmEmoji, Stream, UserMessage, UserProfile class ReactionEmojiTest(ZulipTestCase): def test_missing_emoji(self) -> None: @@ -358,6 +358,33 @@ def test_remove_existing_reaction_with_deactivated_realm_emoji(self) -> None: result = self.api_delete(sender, '/api/v1/messages/1/reactions', reaction_info) self.assert_json_success(result) + def test_add_reaction_in_admins_can_post_and_react_streams(self) -> None: + realm = get_realm('zulip') + sender = self.example_user('iago') + reaction_sender = self.example_user("hamlet") + reaction_sender.role = UserProfile.ROLE_MEMBER + reaction_sender.save() + emoji_code, reaction_type = emoji_name_to_emoji_code(realm, 'smile') + stream = self.make_stream("example1") + stream.save() + msg_id = self.send_stream_message(sender, stream.name, + topic_name="test", content="test") + reaction_info = { + 'emoji_name': 'smile', + 'emoji_code': emoji_code, + 'reaction_type': reaction_type + } + + stream.stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT + stream.save() + result = self.api_post(reaction_sender, '/api/v1/messages/%s/reactions' % (msg_id,), reaction_info) + self.assert_json_error(result, "Only admins can react.") + + stream.stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS + stream.save() + result = self.api_post(reaction_sender, '/api/v1/messages/%s/reactions' % (msg_id,), reaction_info) + self.assert_json_success(result) + class ReactionEventTest(ZulipTestCase): def test_add_event(self) -> None: """ diff --git a/zerver/tests/test_subs.py b/zerver/tests/test_subs.py index 38a5ca4a87a9a8..b73b0ddcb6de4e 100644 --- a/zerver/tests/test_subs.py +++ b/zerver/tests/test_subs.py @@ -806,7 +806,8 @@ def test_non_admin(how_old: int, is_new: bool, policy: int) -> None: {'stream_post_policy': ujson.dumps(policy)}) self.assert_json_error(result, 'Must be an organization administrator') - policies = [Stream.STREAM_POST_POLICY_ADMINS, Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS] + policies = [Stream.STREAM_POST_POLICY_ADMINS, Stream.STREAM_POST_POLICY_RESTRICT_NEW_MEMBERS, + Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT] for policy in policies: test_non_admin(how_old=15, is_new=False, policy=policy) @@ -2638,6 +2639,28 @@ def test_subscribe_to_stream_post_policy_restrict_new_members_stream(self) -> No 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_subscribe_to_stream_post_policy_admins_can_post_and_react_stream(self) -> None: + """Members can subscribe to streams where only admins can post and react + but not create those streams, only realm admins can""" + member = self.example_user("AARON") + result = self.common_subscribe_to_streams(member, ["general"]) + self.assert_json_success(result) + + streams_raw = [{ + 'name': 'new_stream', + 'stream_post_policy': Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT, + }] + with self.assertRaisesRegex( + JsonableError, "User cannot create a stream with these settings."): + list_to_streams(streams_raw, member, autocreate=True) + + admin = self.example_user("iago") + 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, 'new_stream') + self.assertTrue(result[1][0].stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT) + def test_guest_user_subscribe(self) -> None: """Guest users cannot subscribe themselves to anything""" guest_user = self.example_user("polonius") diff --git a/zerver/views/reactions.py b/zerver/views/reactions.py index 31180adddb71c2..1a93f72cf37988 100644 --- a/zerver/views/reactions.py +++ b/zerver/views/reactions.py @@ -8,7 +8,8 @@ from zerver.lib.message import access_message from zerver.lib.request import JsonableError from zerver.lib.response import json_success -from zerver.models import Message, Reaction, UserMessage, UserProfile +from zerver.lib.streams import access_stream_by_id +from zerver.models import Message, Reaction, Recipient, Stream, UserMessage, UserProfile from typing import Optional @@ -29,6 +30,12 @@ def add_reaction(request: HttpRequest, user_profile: UserProfile, message_id: in reaction_type: Optional[str]=REQ(default=None)) -> HttpResponse: message, user_message = access_message(user_profile, message_id) + if message.recipient.type == Recipient.STREAM: + stream, recipient, sub = access_stream_by_id(user_profile, message.recipient.type_id) + if stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS_CAN_POST_AND_REACT: + if not user_profile.is_realm_admin: + raise JsonableError(_("Only admins can react.")) + if emoji_code is None: # The emoji_code argument is only required for rare corner # cases discussed in the long block comment below. For simple