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
# updated_at :datetime not null
# chart_type :integer default("bar"), not null
# groups :string
#
# Indexes
#

View File

@ -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

View File

@ -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"),

View File

@ -17,6 +17,14 @@
valueAttribute="value"}}
</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}}
<div class="input-group poll-select">
<label class="input-group-label">{{i18n 'poll.ui_builder.poll_chart_type.label'}}</label>

View File

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

View File

@ -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));
}
},

View File

@ -31,6 +31,8 @@ en:
title: "Votes are <strong>public</strong>."
results:
groups:
title: "You need to be a member of %{groups} to vote in this poll."
vote:
title: "Results will be shown on <strong>vote</strong>."
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:

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
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)

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.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|

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"))
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)

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",
"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) {