From 63bab32816976db12af911c78a38097a8765fe2a Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 4 Dec 2017 14:47:11 +0100 Subject: [PATCH] FEATURE: introduces minimum trust level for polls (#5391) * FEATURE: introduces minimum trust level for polls This commit makes `poll_enabled` less misleading and introduces `poll_minimum_trust_level_to_create`. If poll are enabled they will always be cooked, and if you have the required trust level you can create polls. As a side effect, it also fixes a bug where rebaking a post created by staff member when `poll_enabled=false` would end up not cooking it. It also adds more tests to ensure settings are respected. * admins should be whitelisted * checks for admin in post validation * test for >= instead of == trust level --- .../initializers/add-poll-ui-builder.js.es6 | 22 +++-- .../lib/discourse-markdown/poll.js.es6 | 5 +- plugins/poll/config/locales/server.en.yml | 5 +- plugins/poll/config/settings.yml | 4 + plugins/poll/lib/post_validator.rb | 25 +++++ plugins/poll/plugin.rb | 5 +- .../spec/controllers/posts_controller_spec.rb | 96 +++++++++++++++++++ .../poll-builder-disabled-test.js.es6 | 41 ++++++++ .../poll-builder-enabled-test.js.es6 | 41 ++++++++ .../display-poll-builder-button.js.es6 | 5 + .../helpers/replace-current-user.js.es6 | 5 + 11 files changed, 241 insertions(+), 13 deletions(-) create mode 100644 plugins/poll/lib/post_validator.rb create mode 100644 plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 create mode 100644 plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 create mode 100644 plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 create mode 100644 plugins/poll/test/javascripts/helpers/replace-current-user.js.es6 diff --git a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 index c2b0a287235..86d5f64f472 100644 --- a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 @@ -1,15 +1,22 @@ import { withPluginApi } from 'discourse/lib/plugin-api'; +import computed from 'ember-addons/ember-computed-decorators'; import showModal from 'discourse/lib/show-modal'; function initializePollUIBuilder(api) { - const siteSettings = api.container.lookup('site-settings:main'); - - if (!siteSettings.poll_enabled && (api.getCurrentUser() && !api.getCurrentUser().staff)) return; - api.modifyClass('controller:composer', { + @computed('siteSettings.poll_enabled', 'siteSettings.poll_minimum_trust_level_to_create') + canBuildPoll(pollEnabled, minimumTrustLevelToCreate) { + return pollEnabled && + this.currentUser && + ( + this.currentUser.admin || + this.currentUser.trust_level >= minimumTrustLevelToCreate + ); + }, + actions: { showPollBuilder() { - showModal("poll-ui-builder").set("toolbarEvent", this.get("toolbarEvent")); + showModal('poll-ui-builder').set('toolbarEvent', this.get('toolbarEvent')); } } }); @@ -18,13 +25,14 @@ function initializePollUIBuilder(api) { return { action: 'showPollBuilder', icon: 'bar-chart-o', - label: 'poll.ui_builder.title' + label: 'poll.ui_builder.title', + condition: 'canBuildPoll' }; }); } export default { - name: "add-poll-ui-builder", + name: 'add-poll-ui-builder', initialize() { withPluginApi('0.8.7', initializePollUIBuilder); diff --git a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 index 442f2a0d242..ca67fa921b4 100644 --- a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 +++ b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 @@ -281,10 +281,7 @@ const rule = { function newApiInit(helper) { helper.registerOptions((opts, siteSettings) => { - const currentUser = (opts.getCurrentUser && opts.getCurrentUser(opts.userId)) || opts.currentUser; - const staff = currentUser && currentUser.staff; - - opts.features.poll = !!siteSettings.poll_enabled || staff; + opts.features.poll = !!siteSettings.poll_enabled; opts.pollMaximumOptions = siteSettings.poll_maximum_options; }); diff --git a/plugins/poll/config/locales/server.en.yml b/plugins/poll/config/locales/server.en.yml index 6ae0bea3c42..1916da774f6 100644 --- a/plugins/poll/config/locales/server.en.yml +++ b/plugins/poll/config/locales/server.en.yml @@ -16,9 +16,10 @@ en: site_settings: - poll_enabled: "Allow users to create polls?" + poll_enabled: "Allow polls?" poll_maximum_options: "Maximum number of options allowed in a poll." poll_edit_window_mins: "Number of minutes after post creation during which polls can be edited." + poll_minimum_trust_level_to_create: "Define the minimum trust level needed to create polls" poll: multiple_polls_without_name: "There are multiple polls without a name. Use the 'name' attribute to uniquely identify your polls." @@ -61,5 +62,7 @@ en: topic_must_be_open_to_toggle_status: "The topic must be open to toggle status." only_staff_or_op_can_toggle_status: "Only a staff member or the original poster can toggle a poll status." + insufficient_trust_level_to_create: "Your trust level (%{current}) is insufficient to create polls (required: %{required})" + email: link_to_poll: "Click to view the poll." diff --git a/plugins/poll/config/settings.yml b/plugins/poll/config/settings.yml index 04bf21f5e99..5f4cfa825b7 100644 --- a/plugins/poll/config/settings.yml +++ b/plugins/poll/config/settings.yml @@ -7,3 +7,7 @@ plugins: client: true poll_edit_window_mins: default: 5 + poll_minimum_trust_level_to_create: + default: 1 + client: true + enum: 'TrustLevelSetting' diff --git a/plugins/poll/lib/post_validator.rb b/plugins/poll/lib/post_validator.rb new file mode 100644 index 00000000000..7e1a5f53ad1 --- /dev/null +++ b/plugins/poll/lib/post_validator.rb @@ -0,0 +1,25 @@ +module DiscoursePoll + class PostValidator + def initialize(post) + @post = post + end + + def validate_post + min_trust_level = SiteSetting.poll_minimum_trust_level_to_create + trusted = @post&.user&.admin || + @post&.user&.trust_level >= TrustLevel[min_trust_level] + + if !trusted + message = I18n.t("poll.insufficient_trust_level_to_create", + current: @post&.user&.trust_level, + required: min_trust_level + ) + + @post.errors.add(:base, message) + return false + end + + true + end + end +end diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index 84eb24adf63..669c72956c2 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -14,12 +14,12 @@ PLUGIN_NAME ||= "discourse_poll".freeze DATA_PREFIX ||= "data-poll-".freeze after_initialize do - module ::DiscoursePoll DEFAULT_POLL_NAME ||= "poll".freeze POLLS_CUSTOM_FIELD ||= "polls".freeze VOTES_CUSTOM_FIELD ||= "polls-votes".freeze + autoload :PostValidator, "#{Rails.root}/plugins/poll/lib/post_validator" autoload :PollsValidator, "#{Rails.root}/plugins/poll/lib/polls_validator" autoload :PollsUpdater, "#{Rails.root}/plugins/poll/lib/polls_updater" @@ -314,6 +314,9 @@ after_initialize do # only care when raw has changed! return unless self.raw_changed? || force + validator = DiscoursePoll::PostValidator.new(self) + return unless validator.validate_post + validator = DiscoursePoll::PollsValidator.new(self) return unless (polls = validator.validate_polls) diff --git a/plugins/poll/spec/controllers/posts_controller_spec.rb b/plugins/poll/spec/controllers/posts_controller_spec.rb index 7a3e39a77cd..637eb66267e 100644 --- a/plugins/poll/spec/controllers/posts_controller_spec.rb +++ b/plugins/poll/spec/controllers/posts_controller_spec.rb @@ -336,4 +336,100 @@ describe PostsController do end + describe "disabled polls" do + before do + SiteSetting.poll_enabled = false + end + + it "doesn’t cook the poll" do + post :create, params: { + title: title, raw: "[poll]\n- A\n- B\n[/poll]" + }, format: :json + + expect(response).to be_success + json = ::JSON.parse(response.body) + expect(json["cooked"]).to eq("

[poll]

\n") + end + end + + describe "insufficient trust level" do + before do + SiteSetting.poll_minimum_trust_level_to_create = 2 + end + + it "invalidates the post" do + log_in_user(Fabricate(:user, trust_level: 1)) + + post :create, params: { + title: title, raw: "[poll]\n- A\n- B\n[/poll]" + }, format: :json + + expect(response).not_to be_success + json = ::JSON.parse(response.body) + expect(json["errors"][0]).to eq( + I18n.t("poll.insufficient_trust_level_to_create", + current: user.trust_level, + required: SiteSetting.poll_minimum_trust_level_to_create + ) + ) + end + end + + describe "equal trust level" do + before do + SiteSetting.poll_minimum_trust_level_to_create = 2 + end + + it "validates the post" do + log_in_user(Fabricate(:user, trust_level: 2)) + + post :create, params: { + title: title, raw: "[poll]\n- A\n- B\n[/poll]" + }, format: :json + + expect(response).to be_success + json = ::JSON.parse(response.body) + expect(json["cooked"]).to match("data-poll-") + expect(json["polls"]["poll"]).to be + end + end + + describe "superior trust level" do + before do + SiteSetting.poll_minimum_trust_level_to_create = 2 + end + + it "validates the post" do + log_in_user(Fabricate(:user, trust_level: 3)) + + post :create, params: { + title: title, raw: "[poll]\n- A\n- B\n[/poll]" + }, format: :json + + expect(response).to be_success + json = ::JSON.parse(response.body) + expect(json["cooked"]).to match("data-poll-") + expect(json["polls"]["poll"]).to be + end + end + + + describe "admin with insufficient trust level" do + before do + SiteSetting.poll_minimum_trust_level_to_create = 2 + end + + it "validates the post" do + log_in_user(Fabricate(:user, admin: true, trust_level: 1)) + + post :create, params: { + title: title, raw: "[poll]\n- A\n- B\n[/poll]" + }, format: :json + + expect(response).to be_success + json = ::JSON.parse(response.body) + expect(json["cooked"]).to match("data-poll-") + expect(json["polls"]["poll"]).to be + end + end end diff --git a/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 new file mode 100644 index 00000000000..cc65ebe6838 --- /dev/null +++ b/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 @@ -0,0 +1,41 @@ +import { acceptance } from "helpers/qunit-helpers"; +import { displayPollBuilderButton } from "discourse/plugins/poll/helpers/display-poll-builder-button"; +import { replaceCurrentUser } from "discourse/plugins/poll/helpers/replace-current-user"; + +acceptance("Poll Builder - polls are disabled", { + loggedIn: true, + settings: { + poll_enabled: false, + poll_minimum_trust_level_to_create: 2 + } +}); + +test("sufficient trust level", (assert) => { + replaceCurrentUser({ admin: false, trust_level: 3 }); + + displayPollBuilderButton(); + + andThen(() => { + assert.ok(!exists("button[title='Build Poll']"), "it hides the builder button"); + }); +}); + +test("insufficient trust level", (assert) => { + replaceCurrentUser({ admin: false, trust_level: 1 }); + + displayPollBuilderButton(); + + andThen(() => { + assert.ok(!exists("button[title='Build Poll']"), "it hides the builder button"); + }); +}); + +test("admin", (assert) => { + replaceCurrentUser({ admin: true }); + + displayPollBuilderButton(); + + andThen(() => { + assert.ok(!exists("button[title='Build Poll']"), "it hides the builder button"); + }); +}); diff --git a/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 new file mode 100644 index 00000000000..f188d71a181 --- /dev/null +++ b/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 @@ -0,0 +1,41 @@ +import { acceptance } from "helpers/qunit-helpers"; +import { displayPollBuilderButton } from "discourse/plugins/poll/helpers/display-poll-builder-button"; +import { replaceCurrentUser } from "discourse/plugins/poll/helpers/replace-current-user"; + +acceptance("Poll Builder - polls are enabled", { + loggedIn: true, + settings: { + poll_enabled: true, + poll_minimum_trust_level_to_create: 1 + } +}); + +test("sufficient trust level", (assert) => { + replaceCurrentUser({ admin: false, trust_level: 1 }); + + displayPollBuilderButton(); + + andThen(() => { + assert.ok(exists("button[title='Build Poll']"), "it shows the builder button"); + }); +}); + +test("insufficient trust level", (assert) => { + replaceCurrentUser({ admin: false, trust_level: 0 }); + + displayPollBuilderButton(); + + andThen(() => { + assert.ok(!exists("button[title='Build Poll']"), "it hides the builder button"); + }); +}); + +test("admin with insufficient trust level", (assert) => { + replaceCurrentUser({ admin: true, trust_level: 0 }); + + displayPollBuilderButton(); + + andThen(() => { + assert.ok(exists("button[title='Build Poll']"), "it shows the builder button"); + }); +}); diff --git a/plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 b/plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 new file mode 100644 index 00000000000..b3dbbd72c2a --- /dev/null +++ b/plugins/poll/test/javascripts/helpers/display-poll-builder-button.js.es6 @@ -0,0 +1,5 @@ +export function displayPollBuilderButton() { + visit("/"); + click("#create-topic"); + click(".d-editor-button-bar .options"); +} diff --git a/plugins/poll/test/javascripts/helpers/replace-current-user.js.es6 b/plugins/poll/test/javascripts/helpers/replace-current-user.js.es6 new file mode 100644 index 00000000000..59161ed6bdb --- /dev/null +++ b/plugins/poll/test/javascripts/helpers/replace-current-user.js.es6 @@ -0,0 +1,5 @@ +export function replaceCurrentUser(properties) { + const currentUser = Discourse.User.current(); + currentUser.setProperties(properties); + Discourse.User.resetCurrent(currentUser); +}