FEATURE: Add 'groups' option to polls (#8469)

This options can be used to restrict polls to certain groups.
This commit is contained in:
Bianca Nenciu 2020-01-28 14:30:04 +02:00 committed by GitHub
parent a9d0d55817
commit 07222af7ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 118 additions and 21 deletions

View File

@ -78,6 +78,7 @@ end
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# chart_type :integer default("bar"), not null # chart_type :integer default("bar"), not null
# groups :string
# #
# Indexes # Indexes
# #

View File

@ -13,7 +13,8 @@ class PollSerializer < ApplicationSerializer
:voters, :voters,
:close, :close,
:preloaded_voters, :preloaded_voters,
:chart_type :chart_type,
:groups
def public def public
true true
@ -35,6 +36,10 @@ class PollSerializer < ApplicationSerializer
object.step.present? && object.number? object.step.present? && object.number?
end end
def include_groups?
groups.present?
end
def options def options
object.poll_options.map { |o| PollOptionSerializer.new(o, root: false).as_json } object.poll_options.map { |o| PollOptionSerializer.new(o, root: false).as_json }
end end

View File

@ -82,6 +82,13 @@ export default Controller.extend({
return options; 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") @computed("pollType", "regularPollType")
isRegular(pollType, regularPollType) { isRegular(pollType, regularPollType) {
return pollType === regularPollType; return pollType === regularPollType;
@ -184,6 +191,7 @@ export default Controller.extend({
"pollMin", "pollMin",
"pollMax", "pollMax",
"pollStep", "pollStep",
"pollGroups",
"autoClose", "autoClose",
"chartType", "chartType",
"date", "date",
@ -199,6 +207,7 @@ export default Controller.extend({
pollMin, pollMin,
pollMax, pollMax,
pollStep, pollStep,
pollGroups,
autoClose, autoClose,
chartType, chartType,
date, date,
@ -228,6 +237,7 @@ export default Controller.extend({
if (publicPoll) pollHeader += ` public=true`; if (publicPoll) pollHeader += ` public=true`;
if (chartType && pollType !== "number") if (chartType && pollType !== "number")
pollHeader += ` chartType=${chartType}`; pollHeader += ` chartType=${chartType}`;
if (pollGroups) pollHeader += ` groups=${pollGroups}`;
if (autoClose) { if (autoClose) {
let closeDate = moment( let closeDate = moment(
date + " " + time, date + " " + time,
@ -323,6 +333,7 @@ export default Controller.extend({
pollStep: 1, pollStep: 1,
autoClose: false, autoClose: false,
chartType: BAR_CHART_TYPE, chartType: BAR_CHART_TYPE,
pollGroups: null,
date: moment() date: moment()
.add(1, "day") .add(1, "day")
.format("YYYY-MM-DD"), .format("YYYY-MM-DD"),

View File

@ -17,6 +17,14 @@
valueAttribute="value"}} valueAttribute="value"}}
</div> </div>
<div class="input-group poll-select">
<label class="input-group-label">{{i18n 'poll.ui_builder.poll_groups.label'}}</label>
{{combo-box content=siteGroups
value=pollGroups
allowInitialValueMutation=true
valueAttribute="value"}}
</div>
{{#unless isNumber}} {{#unless isNumber}}
<div class="input-group poll-select"> <div class="input-group poll-select">
<label class="input-group-label">{{i18n 'poll.ui_builder.poll_chart_type.label'}}</label> <label class="input-group-label">{{i18n 'poll.ui_builder.poll_chart_type.label'}}</label>

View File

@ -11,6 +11,7 @@ const WHITELISTED_ATTRIBUTES = [
"public", "public",
"results", "results",
"chartType", "chartType",
"groups",
"status", "status",
"step", "step",
"type" "type"

View File

@ -333,16 +333,43 @@ createWidget("discourse-poll-container", {
: "discourse-poll-pie-chart"; : "discourse-poll-pie-chart";
return this.attach(resultsWidget, attrs); return this.attach(resultsWidget, attrs);
} else if (options) { } else if (options) {
return h( const contents = [];
"ul",
options.map(option => { const pollGroups =
return this.attach("discourse-poll-option", { poll.groups && poll.groups.split(",").map(g => g.toLowerCase());
option,
isMultiple: attrs.isMultiple, const userGroups =
vote: attrs.vote 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"); 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) { toggleOption(option) {
const { attrs } = this; const { attrs } = this;
@ -961,20 +998,13 @@ export default createWidget("discourse-poll", {
if (!this.currentUser) return this.showLogin(); if (!this.currentUser) return this.showLogin();
const { vote } = attrs; const { vote } = attrs;
const chosenIdx = vote.indexOf(option.id);
if (!this.isMultiple()) { if (!this.isMultiple()) {
vote.length = 0; vote.length = 0;
} }
if (chosenIdx !== -1) { this._toggleOption(option);
vote.splice(chosenIdx, 1);
} else {
vote.push(option.id);
}
if (!this.isMultiple()) { if (!this.isMultiple()) {
return this.castVotes(); return this.castVotes().catch(() => this._toggleOption(option));
} }
}, },

View File

@ -31,6 +31,8 @@ en:
title: "Votes are <strong>public</strong>." title: "Votes are <strong>public</strong>."
results: results:
groups:
title: "You need to be a member of %{groups} to vote in this poll."
vote: vote:
title: "Results will be shown on <strong>vote</strong>." title: "Results will be shown on <strong>vote</strong>."
closed: closed:
@ -112,6 +114,8 @@ en:
vote: On vote vote: On vote
closed: When closed closed: When closed
staff: Staff only staff: Staff only
poll_groups:
label: Allowed groups
poll_chart_type: poll_chart_type:
label: Chart type label: Chart type
poll_config: poll_config:

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddGroupNameToPolls < ActiveRecord::Migration[5.2]
def change
add_column :polls, :groups, :string
end
end

View File

@ -3,7 +3,7 @@
module DiscoursePoll module DiscoursePoll
class PollsUpdater 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) def self.update(post, polls)
::Poll.transaction do ::Poll.transaction do
@ -38,6 +38,7 @@ module DiscoursePoll
attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret" attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret"
attributes["close_at"] = Time.zone.parse(new_poll["close"]) rescue nil attributes["close_at"] = Time.zone.parse(new_poll["close"]) rescue nil
attributes["status"] = old_poll["status"] attributes["status"] = old_poll["status"]
attributes["groups"] = new_poll["groups"]
poll = ::Poll.new(attributes) poll = ::Poll.new(attributes)
if is_different?(old_poll, poll, new_poll_options) if is_different?(old_poll, poll, new_poll_options)

View File

@ -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.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? 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 # remove options that aren't available in the poll
available_options = poll.poll_options.map { |o| o.digest }.to_set available_options = poll.poll_options.map { |o| o.digest }.to_set
options.select! { |o| available_options.include?(o) } options.select! { |o| available_options.include?(o) }
@ -322,7 +330,8 @@ after_initialize do
min: poll["min"], min: poll["min"],
max: poll["max"], max: poll["max"],
step: poll["step"], step: poll["step"],
chart_type: poll["charttype"] || "bar" chart_type: poll["charttype"] || "bar",
groups: poll["groups"]
) )
poll["options"].each do |option| poll["options"].each do |option|

View File

@ -186,6 +186,18 @@ describe ::DiscoursePoll::PollsController do
expect(json["errors"][0]).to eq(I18n.t("poll.poll_must_be_open_to_vote")) expect(json["errors"][0]).to eq(I18n.t("poll.poll_must_be_open_to_vote"))
end 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 it "doesn't discard anonymous votes when someone votes" do
the_poll = poll.polls.first the_poll = poll.polls.first
the_poll.update_attribute(:anonymous_voters, 17) the_poll.update_attribute(:anonymous_voters, 17)

View File

@ -283,6 +283,14 @@ test("regular pollOutput", function(assert) {
"[poll type=regular public=true chartType=bar]\n* 1\n* 2\n[/poll]\n", "[poll type=regular public=true chartType=bar]\n* 1\n* 2\n[/poll]\n",
"it should return the right output" "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) { test("multiple pollOutput", function(assert) {