diff --git a/plugins/poll/app/models/poll.rb b/plugins/poll/app/models/poll.rb index 6c007648ae2..fcf71f9158b 100644 --- a/plugins/poll/app/models/poll.rb +++ b/plugins/poll/app/models/poll.rb @@ -78,6 +78,7 @@ end # created_at :datetime not null # updated_at :datetime not null # chart_type :integer default("bar"), not null +# groups :string # # Indexes # diff --git a/plugins/poll/app/serializers/poll_serializer.rb b/plugins/poll/app/serializers/poll_serializer.rb index 3d73264d036..53b7c1d5e2b 100644 --- a/plugins/poll/app/serializers/poll_serializer.rb +++ b/plugins/poll/app/serializers/poll_serializer.rb @@ -13,7 +13,8 @@ class PollSerializer < ApplicationSerializer :voters, :close, :preloaded_voters, - :chart_type + :chart_type, + :groups def public true @@ -35,6 +36,10 @@ class PollSerializer < ApplicationSerializer object.step.present? && object.number? end + def include_groups? + groups.present? + end + def options object.poll_options.map { |o| PollOptionSerializer.new(o, root: false).as_json } end diff --git a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 index 4e3c7e0391c..2c6e8335d63 100644 --- a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 +++ b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 @@ -82,6 +82,13 @@ export default Controller.extend({ return options; }, + @computed("site.groups") + siteGroups(groups) { + const values = [{ name: "", value: null }]; + groups.forEach(g => values.push({ name: g.name, value: g.name })); + return values; + }, + @computed("pollType", "regularPollType") isRegular(pollType, regularPollType) { return pollType === regularPollType; @@ -184,6 +191,7 @@ export default Controller.extend({ "pollMin", "pollMax", "pollStep", + "pollGroups", "autoClose", "chartType", "date", @@ -199,6 +207,7 @@ export default Controller.extend({ pollMin, pollMax, pollStep, + pollGroups, autoClose, chartType, date, @@ -228,6 +237,7 @@ export default Controller.extend({ if (publicPoll) pollHeader += ` public=true`; if (chartType && pollType !== "number") pollHeader += ` chartType=${chartType}`; + if (pollGroups) pollHeader += ` groups=${pollGroups}`; if (autoClose) { let closeDate = moment( date + " " + time, @@ -323,6 +333,7 @@ export default Controller.extend({ pollStep: 1, autoClose: false, chartType: BAR_CHART_TYPE, + pollGroups: null, date: moment() .add(1, "day") .format("YYYY-MM-DD"), diff --git a/plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs b/plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs index 739e0a40dcd..21615b87c85 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs +++ b/plugins/poll/assets/javascripts/discourse/templates/modal/poll-ui-builder.hbs @@ -17,6 +17,14 @@ valueAttribute="value"}} +
+ + {{combo-box content=siteGroups + value=pollGroups + allowInitialValueMutation=true + valueAttribute="value"}} +
+ {{#unless isNumber}}
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 6b7adcba89b..14de1cae4af 100644 --- a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 +++ b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 @@ -11,6 +11,7 @@ const WHITELISTED_ATTRIBUTES = [ "public", "results", "chartType", + "groups", "status", "step", "type" diff --git a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 index 402f4519e11..03fe68805cb 100644 --- a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 +++ b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 @@ -333,16 +333,43 @@ createWidget("discourse-poll-container", { : "discourse-poll-pie-chart"; return this.attach(resultsWidget, attrs); } else if (options) { - return h( - "ul", - options.map(option => { - return this.attach("discourse-poll-option", { - option, - isMultiple: attrs.isMultiple, - vote: attrs.vote - }); - }) + const contents = []; + + const pollGroups = + poll.groups && poll.groups.split(",").map(g => g.toLowerCase()); + + const userGroups = + this.currentUser && + this.currentUser.groups && + this.currentUser.groups.map(g => g.name.toLowerCase()); + + if ( + pollGroups && + userGroups && + !pollGroups.some(g => userGroups.includes(g)) + ) { + contents.push( + h( + "div.alert.alert-danger", + I18n.t("poll.results.groups.title", { groups: poll.groups }) + ) + ); + } + + contents.push( + h( + "ul", + options.map(option => { + return this.attach("discourse-poll-option", { + option, + isMultiple: attrs.isMultiple, + vote: attrs.vote + }); + }) + ) ); + + return contents; } } }); @@ -954,6 +981,16 @@ export default createWidget("discourse-poll", { this.register.lookup("route:application").send("showLogin"); }, + _toggleOption(option) { + const { vote } = this.attrs; + const chosenIdx = vote.indexOf(option.id); + if (chosenIdx !== -1) { + vote.splice(chosenIdx, 1); + } else { + vote.push(option.id); + } + }, + toggleOption(option) { const { attrs } = this; @@ -961,20 +998,13 @@ export default createWidget("discourse-poll", { if (!this.currentUser) return this.showLogin(); const { vote } = attrs; - const chosenIdx = vote.indexOf(option.id); - if (!this.isMultiple()) { vote.length = 0; } - if (chosenIdx !== -1) { - vote.splice(chosenIdx, 1); - } else { - vote.push(option.id); - } - + this._toggleOption(option); if (!this.isMultiple()) { - return this.castVotes(); + return this.castVotes().catch(() => this._toggleOption(option)); } }, diff --git a/plugins/poll/config/locales/client.en.yml b/plugins/poll/config/locales/client.en.yml index 70c89983cfd..c776f7feba6 100644 --- a/plugins/poll/config/locales/client.en.yml +++ b/plugins/poll/config/locales/client.en.yml @@ -31,6 +31,8 @@ en: title: "Votes are public." results: + groups: + title: "You need to be a member of %{groups} to vote in this poll." vote: title: "Results will be shown on vote." closed: @@ -112,6 +114,8 @@ en: vote: On vote closed: When closed staff: Staff only + poll_groups: + label: Allowed groups poll_chart_type: label: Chart type poll_config: diff --git a/plugins/poll/db/migrate/20191206123012_add_group_name_to_polls.rb b/plugins/poll/db/migrate/20191206123012_add_group_name_to_polls.rb new file mode 100644 index 00000000000..7cad643a356 --- /dev/null +++ b/plugins/poll/db/migrate/20191206123012_add_group_name_to_polls.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddGroupNameToPolls < ActiveRecord::Migration[5.2] + def change + add_column :polls, :groups, :string + end +end diff --git a/plugins/poll/lib/polls_updater.rb b/plugins/poll/lib/polls_updater.rb index 343611ee65a..1b2e62f8a14 100644 --- a/plugins/poll/lib/polls_updater.rb +++ b/plugins/poll/lib/polls_updater.rb @@ -3,7 +3,7 @@ module DiscoursePoll class PollsUpdater - POLL_ATTRIBUTES ||= %w{close_at max min results status step type visibility} + POLL_ATTRIBUTES ||= %w{close_at max min results status step type visibility groups} def self.update(post, polls) ::Poll.transaction do @@ -38,6 +38,7 @@ module DiscoursePoll attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret" attributes["close_at"] = Time.zone.parse(new_poll["close"]) rescue nil attributes["status"] = old_poll["status"] + attributes["groups"] = new_poll["groups"] poll = ::Poll.new(attributes) if is_different?(old_poll, poll, new_poll_options) diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index b7d4d2f7c98..d1b54eeeeb7 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -71,6 +71,14 @@ after_initialize do raise StandardError.new I18n.t("poll.no_poll_with_this_name", name: poll_name) unless poll raise StandardError.new I18n.t("poll.poll_must_be_open_to_vote") if poll.is_closed? + if poll.groups + poll_groups = poll.groups.split(",").map(&:downcase) + user_groups = user.groups.map { |g| g.name.downcase } + if (poll_groups & user_groups).empty? + raise StandardError.new I18n.t("js.poll.results.groups.title", group: poll.groups) + end + end + # remove options that aren't available in the poll available_options = poll.poll_options.map { |o| o.digest }.to_set options.select! { |o| available_options.include?(o) } @@ -322,7 +330,8 @@ after_initialize do min: poll["min"], max: poll["max"], step: poll["step"], - chart_type: poll["charttype"] || "bar" + chart_type: poll["charttype"] || "bar", + groups: poll["groups"] ) poll["options"].each do |option| diff --git a/plugins/poll/spec/controllers/polls_controller_spec.rb b/plugins/poll/spec/controllers/polls_controller_spec.rb index 78f86e30288..4236e6c51d2 100644 --- a/plugins/poll/spec/controllers/polls_controller_spec.rb +++ b/plugins/poll/spec/controllers/polls_controller_spec.rb @@ -186,6 +186,18 @@ describe ::DiscoursePoll::PollsController do expect(json["errors"][0]).to eq(I18n.t("poll.poll_must_be_open_to_vote")) end + it "ensures user has required trust level" do + poll = create_post(raw: "[poll groups=#{Fabricate(:group).name}]\n- A\n- B\n[/poll]") + + put :vote, params: { + post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] + }, format: :json + + expect(response.status).not_to eq(200) + json = ::JSON.parse(response.body) + expect(json["errors"][0]).to eq(I18n.t("js.poll.results.groups.title", trust_level: 2)) + end + it "doesn't discard anonymous votes when someone votes" do the_poll = poll.polls.first the_poll.update_attribute(:anonymous_voters, 17) diff --git a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 index 5474b12b096..24601d3c0f0 100644 --- a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 +++ b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 @@ -283,6 +283,14 @@ test("regular pollOutput", function(assert) { "[poll type=regular public=true chartType=bar]\n* 1\n* 2\n[/poll]\n", "it should return the right output" ); + + controller.set("pollGroups", "test"); + + assert.equal( + controller.get("pollOutput"), + "[poll type=regular public=true chartType=bar groups=test]\n* 1\n* 2\n[/poll]\n", + "it should return the right output" + ); }); test("multiple pollOutput", function(assert) {