diff --git a/plugins/poll/assets/javascripts/components/poll-results-number-voters.js.es6 b/plugins/poll/assets/javascripts/components/poll-results-number-voters.js.es6
new file mode 100644
index 00000000000..63449af066a
--- /dev/null
+++ b/plugins/poll/assets/javascripts/components/poll-results-number-voters.js.es6
@@ -0,0 +1,31 @@
+import computed from 'ember-addons/ember-computed-decorators';
+import User from 'discourse/models/user';
+import PollVoters from 'discourse/plugins/poll/components/poll-voters';
+
+export default PollVoters.extend({
+ @computed("pollsVoters", "poll.options", "showMore", "isExpanded", "numOfVotersToShow")
+ users(pollsVoters, options, showMore, isExpanded, numOfVotersToShow) {
+ var users = [];
+ var voterIds = [];
+ const shouldLimit = showMore && !isExpanded;
+
+ options.forEach(option => {
+ option.voter_ids.forEach(voterId => {
+ if (shouldLimit) {
+ if (!(users.length > numOfVotersToShow - 1)) {
+ users.push(pollsVoters[voterId]);
+ }
+ } else {
+ users.push(pollsVoters[voterId]);
+ }
+ })
+ });
+
+ return users;
+ },
+
+ @computed("pollsVoters", "numOfVotersToShow")
+ showMore(pollsVoters, numOfVotersToShow) {
+ return !(Object.keys(pollsVoters).length < numOfVotersToShow);
+ }
+});
diff --git a/plugins/poll/assets/javascripts/components/poll-results-number.js.es6 b/plugins/poll/assets/javascripts/components/poll-results-number.js.es6
index 42087e2933b..5718642b327 100644
--- a/plugins/poll/assets/javascripts/components/poll-results-number.js.es6
+++ b/plugins/poll/assets/javascripts/components/poll-results-number.js.es6
@@ -1,23 +1,27 @@
import round from "discourse/lib/round";
+import computed from 'ember-addons/ember-computed-decorators';
export default Em.Component.extend({
tagName: "span",
- totalScore: function() {
+ @computed("poll.options.@each.{html,votes}")
+ totalScore() {
return _.reduce(this.get("poll.options"), function(total, o) {
const value = parseInt(o.get("html"), 10),
votes = parseInt(o.get("votes"), 10);
return total + value * votes;
}, 0);
- }.property("poll.options.@each.{html,votes}"),
+ },
- average: function() {
+ @computed("totalScore", "poll.voters")
+ average() {
const voters = this.get("poll.voters");
return voters === 0 ? 0 : round(this.get("totalScore") / voters, -2);
- }.property("totalScore", "poll.voters"),
+ },
- averageRating: function() {
+ @computed("average")
+ averageRating() {
return I18n.t("poll.average_rating", { average: this.get("average") });
- }.property("average"),
+ },
});
diff --git a/plugins/poll/assets/javascripts/components/poll-results-standard-voters.js.es6 b/plugins/poll/assets/javascripts/components/poll-results-standard-voters.js.es6
new file mode 100644
index 00000000000..1e51dc23c44
--- /dev/null
+++ b/plugins/poll/assets/javascripts/components/poll-results-standard-voters.js.es6
@@ -0,0 +1,25 @@
+import computed from 'ember-addons/ember-computed-decorators';
+import User from 'discourse/models/user';
+import PollVoters from 'discourse/plugins/poll/components/poll-voters';
+
+export default PollVoters.extend({
+ @computed("pollsVoters", "option.voter_ids", "showMore", "isExpanded", "numOfVotersToShow")
+ users(pollsVoters, voterIds, showMore, isExpanded, numOfVotersToShow) {
+ var users = [];
+
+ if (showMore && !isExpanded) {
+ voterIds = voterIds.slice(0, numOfVotersToShow);
+ }
+
+ voterIds.forEach(voterId => {
+ users.push(pollsVoters[voterId]);
+ });
+
+ return users;
+ },
+
+ @computed("option.votes", "numOfVotersToShow")
+ showMore(numOfVotes, numOfVotersToShow) {
+ return !(numOfVotes < numOfVotersToShow);
+ }
+});
diff --git a/plugins/poll/assets/javascripts/components/poll-voters.js.es6 b/plugins/poll/assets/javascripts/components/poll-voters.js.es6
new file mode 100644
index 00000000000..b580f111821
--- /dev/null
+++ b/plugins/poll/assets/javascripts/components/poll-voters.js.es6
@@ -0,0 +1,13 @@
+export default Ember.Component.extend({
+ layoutName: "components/poll-voters",
+ tagName: 'ul',
+ classNames: ["poll-voters-list"],
+ isExpanded: false,
+ numOfVotersToShow: 20,
+
+ actions: {
+ toggleExpand() {
+ this.toggleProperty("isExpanded");
+ }
+ }
+});
diff --git a/plugins/poll/assets/javascripts/controllers/poll.js.es6 b/plugins/poll/assets/javascripts/controllers/poll.js.es6
index 14a5079408f..b986c4c9fc2 100644
--- a/plugins/poll/assets/javascripts/controllers/poll.js.es6
+++ b/plugins/poll/assets/javascripts/controllers/poll.js.es6
@@ -6,6 +6,7 @@ export default Ember.Controller.extend({
isNumber: Ember.computed.equal("poll.type", "number"),
isRandom : Ember.computed.equal("poll.order", "random"),
isClosed: Ember.computed.equal("poll.status", "closed"),
+ pollsVoters: Ember.computed.alias("post.polls_voters"),
// shows the results when
// - poll is closed
@@ -145,8 +146,16 @@ export default Ember.Controller.extend({
options: this.get("selectedOptions"),
}
}).then(results => {
- this.setProperties({ vote: results.vote, showResults: true });
- this.set("model", Em.Object.create(results.poll));
+ const poll = results.poll;
+ const votes = results.vote;
+ const currentUser = this.currentUser;
+
+ this.setProperties({ vote: votes, showResults: true });
+ this.set("model", Em.Object.create(poll));
+
+ if (poll.public) {
+ this.get("pollsVoters")[currentUser.get("id")] = currentUser;
+ }
}).catch(() => {
bootbox.alert(I18n.t("poll.error_while_casting_votes"));
}).finally(() => {
diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs
index b6cf23314d9..48e73ff2fc8 100644
--- a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs
+++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-number.hbs
@@ -1 +1,5 @@
{{{averageRating}}}
+
+{{#if poll.public}}
+ {{poll-results-number-voters poll=poll pollsVoters=pollsVoters}}
+{{/if}}
diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs
index d625b76cd2d..e412d61a772 100644
--- a/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs
+++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-results-standard.hbs
@@ -9,5 +9,9 @@
+
+ {{#if poll.public}}
+ {{poll-results-standard-voters option=option pollsVoters=pollsVoters}}
+ {{/if}}
{{/each}}
diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-voters.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-voters.hbs
new file mode 100644
index 00000000000..afa3f26c4b9
--- /dev/null
+++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-voters.hbs
@@ -0,0 +1,19 @@
+
diff --git a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs
index 418c5b6355f..1d1e22ad182 100644
--- a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs
+++ b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs
@@ -2,9 +2,9 @@
{{#if showingResults}}
{{#if isNumber}}
- {{poll-results-number poll=poll}}
+ {{poll-results-number poll=poll pollsVoters=pollsVoters}}
{{else}}
- {{poll-results-standard poll=poll}}
+ {{poll-results-standard poll=poll pollsVoters=pollsVoters}}
{{/if}}
{{else}}
diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
index aae84ce3f76..0b7919258bf 100644
--- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
+++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
@@ -1,11 +1,17 @@
import { withPluginApi } from 'discourse/lib/plugin-api';
+import { observes } from "ember-addons/ember-computed-decorators";
-function createPollView(container, post, poll, vote) {
+function createPollView(container, post, poll, vote, publicPoll) {
const controller = container.lookup("controller:poll", { singleton: false });
const view = container.lookup("view:poll");
- controller.set("vote", vote);
- controller.setProperties({ model: poll, post });
+ controller.setProperties({
+ model: poll,
+ vote: vote,
+ public: publicPoll,
+ post
+ });
+
view.set("controller", controller);
return view;
@@ -23,6 +29,10 @@ function initializePolls(api) {
const post = this.get('model.postStream').findLoadedPost(msg.post_id);
if (post) {
post.set('polls', msg.polls);
+
+ if (msg.user) {
+ post.set(`polls_voters.${msg.user.id}`, msg.user);
+ }
}
});
},
@@ -38,7 +48,8 @@ function initializePolls(api) {
pollsObject: null,
// we need a proper ember object so it is bindable
- pollsChanged: function(){
+ @observes("polls")
+ pollsChanged() {
const polls = this.get("polls");
if (polls) {
this._polls = this._polls || {};
@@ -52,7 +63,7 @@ function initializePolls(api) {
});
this.set("pollsObject", this._polls);
}
- }.observes("polls")
+ }
});
function cleanUpPollViews() {
@@ -69,6 +80,7 @@ function initializePolls(api) {
const post = helper.getModel();
api.preventCloak(post.id);
const votes = post.get('polls_votes') || {};
+ post.set("polls_voters", (post.get("polls_voters") || {}));
post.pollsChanged();
@@ -82,8 +94,16 @@ function initializePolls(api) {
const $poll = $(pollElem);
const pollName = $poll.data("poll-name");
+ const publicPoll = $poll.data("poll-public");
const pollId = `${pollName}-${post.id}`;
- const pollView = createPollView(helper.container, post, polls[pollName], votes[pollName]);
+
+ const pollView = createPollView(
+ helper.container,
+ post,
+ polls[pollName],
+ votes[pollName],
+ publicPoll
+ );
$poll.replaceWith($div);
Em.run.next(() => pollView.renderer.replaceIn(pollView, $div[0]));
diff --git a/plugins/poll/assets/javascripts/poll_dialect.js b/plugins/poll/assets/javascripts/poll_dialect.js
index bc9b6585ede..538518fe7dd 100644
--- a/plugins/poll/assets/javascripts/poll_dialect.js
+++ b/plugins/poll/assets/javascripts/poll_dialect.js
@@ -5,7 +5,7 @@
var DATA_PREFIX = "data-poll-";
var DEFAULT_POLL_NAME = "poll";
- var WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", "status"];
+ var WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", "status", "public"];
var ATTRIBUTES_REGEX = new RegExp("(" + WHITELISTED_ATTRIBUTES.join("|") + ")=['\"]?[^\\s\\]]+['\"]?", "g");
diff --git a/plugins/poll/assets/javascripts/views/poll.js.es6 b/plugins/poll/assets/javascripts/views/poll.js.es6
index bad40cfebff..ac739b3f429 100644
--- a/plugins/poll/assets/javascripts/views/poll.js.es6
+++ b/plugins/poll/assets/javascripts/views/poll.js.es6
@@ -3,17 +3,12 @@ import { on } from "ember-addons/ember-computed-decorators";
export default Em.View.extend({
templateName: "poll",
classNames: ["poll"],
- attributeBindings: ["data-poll-type", "data-poll-name", "data-poll-status"],
+ attributeBindings: ["data-poll-type", "data-poll-name", "data-poll-status", "data-poll-public"],
poll: Em.computed.alias("controller.poll"),
"data-poll-type": Em.computed.alias("poll.type"),
"data-poll-name": Em.computed.alias("poll.name"),
"data-poll-status": Em.computed.alias("poll.status"),
-
- @on("didInsertElement")
- _fixPollContainerHeight() {
- const pollContainer = this.$(".poll-container");
- pollContainer.height(pollContainer.height());
- }
+ "data-poll-public": Em.computed.alias("poll.public")
});
diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss
index b15a6574009..5db25555550 100644
--- a/plugins/poll/assets/stylesheets/common/poll.scss
+++ b/plugins/poll/assets/stylesheets/common/poll.scss
@@ -92,6 +92,18 @@ div.poll {
}
}
+ .poll-voters-list {
+ li {
+ display: inline;
+ }
+
+ margin: 5px 0;
+ }
+
+ .poll-voters-toggle-expand {
+ text-align: center;
+ }
+
.results {
.option {
@@ -120,9 +132,11 @@ div.poll {
&[data-poll-type="number"] {
- li {
+ li[data-poll-option-id] {
display: inline-block;
- margin: 0 12px 15px 5px;
+ text-align: center;
+ width: 25px;
+ margin-right: 5px;
}
}
diff --git a/plugins/poll/lib/polls_updater.rb b/plugins/poll/lib/polls_updater.rb
index 817071b7ab1..2f05f91c706 100644
--- a/plugins/poll/lib/polls_updater.rb
+++ b/plugins/poll/lib/polls_updater.rb
@@ -1,6 +1,6 @@
module DiscoursePoll
class PollsUpdater
- VALID_POLLS_CONFIGS = %w{type min max}.map(&:freeze)
+ VALID_POLLS_CONFIGS = %w{type min max public}.map(&:freeze)
def self.update(post, polls)
# load previous polls
@@ -53,11 +53,16 @@ module DiscoursePoll
polls[poll_name]["anonymous_voters"] = previous_polls[poll_name]["anonymous_voters"] if previous_polls[poll_name].has_key?("anonymous_voters")
previous_options = previous_polls[poll_name]["options"]
+ public_poll = polls[poll_name]["public"] == "true"
polls[poll_name]["options"].each_with_index do |option, index|
previous_option = previous_options[index]
option["votes"] = previous_option["votes"]
option["anonymous_votes"] = previous_option["anonymous_votes"] if previous_option.has_key?("anonymous_votes")
+
+ if public_poll && previous_option.has_key?("voter_ids")
+ option["voter_ids"] = previous_option["voter_ids"]
+ end
end
end
diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb
index 251ec78b0cc..26b825dac6e 100644
--- a/plugins/poll/plugin.rb
+++ b/plugins/poll/plugin.rb
@@ -57,6 +57,7 @@ after_initialize do
raise StandardError.new I18n.t("poll.no_poll_with_this_name", name: poll_name) if poll.blank?
raise StandardError.new I18n.t("poll.poll_must_be_open_to_vote") if poll["status"] != "open"
+ public_poll = (poll["public"] == "true")
# remove options that aren't available in the poll
available_options = poll["options"].map { |o| o["id"] }.to_set
@@ -80,12 +81,30 @@ after_initialize do
poll["options"].each do |option|
anonymous_votes = option["anonymous_votes"] || 0
option["votes"] = all_options[option["id"]] + anonymous_votes
+
+ if public_poll
+ option["voter_ids"] ||= []
+
+ if options.include?(option["id"])
+ option["voter_ids"] << user_id if !option["voter_ids"].include?(user_id)
+ else
+ option["voter_ids"].delete(user_id)
+ end
+ end
end
post.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD] = polls
post.save_custom_fields(true)
- MessageBus.publish("/polls/#{post.topic_id}", { post_id: post_id, polls: polls })
+ payload = { post_id: post_id, polls: polls }
+
+ if public_poll
+ payload.merge!(
+ user: UserNameSerializer.new(User.find(user_id)).serializable_hash
+ )
+ end
+
+ MessageBus.publish("/polls/#{post.topic_id}", payload)
return [poll, options]
end
@@ -195,7 +214,6 @@ after_initialize do
render_json_error e.message
end
end
-
end
DiscoursePoll::Engine.routes.draw do
@@ -271,11 +289,36 @@ after_initialize do
add_to_serializer(:post, :polls, false) { post_custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD] }
add_to_serializer(:post, :include_polls?) { post_custom_fields.present? && post_custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD].present? }
- add_to_serializer(:post, :polls_votes, false) { post_custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD]["#{scope.user.id}"] }
+ add_to_serializer(:post, :polls_votes, false) do
+ post_custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD]["#{scope.user.id}"]
+ end
+
add_to_serializer(:post, :include_polls_votes?) do
return unless scope.user
return unless post_custom_fields.present?
return unless post_custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD].present?
post_custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD].has_key?("#{scope.user.id}")
end
+
+ add_to_serializer(:post, :polls_voters) do
+ voters = {}
+
+ user_ids = post_custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD].keys
+
+ User.where(id: user_ids).map do |user|
+ voters[user.id] = UserNameSerializer.new(user).serializable_hash
+ end
+
+ voters
+ end
+
+ add_to_serializer(:post, :include_polls_voters?) do
+ return unless post_custom_fields.present?
+ return unless post_custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD].present?
+ return unless post_custom_fields[DiscoursePoll::VOTES_CUSTOM_FIELD].present?
+
+ post_custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD].any? do |_, value|
+ value["public"] == "true"
+ end
+ end
end
diff --git a/plugins/poll/spec/controllers/polls_controller_spec.rb b/plugins/poll/spec/controllers/polls_controller_spec.rb
index 8a8ce7190e5..8dc6a66cfa6 100644
--- a/plugins/poll/spec/controllers/polls_controller_spec.rb
+++ b/plugins/poll/spec/controllers/polls_controller_spec.rb
@@ -98,6 +98,49 @@ describe ::DiscoursePoll::PollsController do
expect(json["poll"]["options"][0]["votes"]).to eq(12)
expect(json["poll"]["options"][1]["votes"]).to eq(6)
end
+
+ it "tracks the users ids for public polls" do
+ public_poll = Fabricate(:post, topic_id: topic.id, user_id: user.id, raw: "[poll public=true]\n- A\n- B\n[/poll]")
+ body = { post_id: public_poll.id, poll_name: "poll" }
+
+ message = MessageBus.track_publish do
+ xhr :put, :vote, body.merge(options: ["5c24fc1df56d764b550ceae1b9319125"])
+ end.first
+
+ expect(response).to be_success
+
+ json = ::JSON.parse(response.body)
+ expect(json["poll"]["voters"]).to eq(1)
+ expect(json["poll"]["options"][0]["votes"]).to eq(1)
+ expect(json["poll"]["options"][1]["votes"]).to eq(0)
+ expect(json["poll"]["options"][0]["voter_ids"]).to eq([user.id])
+ expect(json["poll"]["options"][1]["voter_ids"]).to eq([])
+ expect(message.data[:post_id].to_i).to eq(public_poll.id)
+ expect(message.data[:user][:id].to_i).to eq(user.id)
+
+ xhr :put, :vote, body.merge(options: ["e89dec30bbd9bf50fabf6a05b4324edf"])
+ expect(response).to be_success
+
+ json = ::JSON.parse(response.body)
+ expect(json["poll"]["voters"]).to eq(1)
+ expect(json["poll"]["options"][0]["votes"]).to eq(0)
+ expect(json["poll"]["options"][1]["votes"]).to eq(1)
+ expect(json["poll"]["options"][0]["voter_ids"]).to eq([])
+ expect(json["poll"]["options"][1]["voter_ids"]).to eq([user.id])
+
+ another_user = Fabricate(:user)
+ log_in_user(another_user)
+
+ xhr :put, :vote, body.merge(options: ["e89dec30bbd9bf50fabf6a05b4324edf", "5c24fc1df56d764b550ceae1b9319125"])
+ expect(response).to be_success
+
+ json = ::JSON.parse(response.body)
+ expect(json["poll"]["voters"]).to eq(2)
+ expect(json["poll"]["options"][0]["votes"]).to eq(1)
+ expect(json["poll"]["options"][1]["votes"]).to eq(2)
+ expect(json["poll"]["options"][0]["voter_ids"]).to eq([another_user.id])
+ expect(json["poll"]["options"][1]["voter_ids"]).to eq([user.id, another_user.id])
+ end
end
describe "#toggle_status" do
diff --git a/plugins/poll/spec/lib/polls_updater_spec.rb b/plugins/poll/spec/lib/polls_updater_spec.rb
index 125d5884581..d2b89c0911a 100644
--- a/plugins/poll/spec/lib/polls_updater_spec.rb
+++ b/plugins/poll/spec/lib/polls_updater_spec.rb
@@ -89,6 +89,69 @@ describe DiscoursePoll::PollsUpdater do
end
end
+ context "public polls" do
+ let(:post) do
+ raw = <<-RAW.strip_heredoc
+ [poll public=true]
+ - A
+ - B
+ [/poll]
+ RAW
+
+ Fabricate(:post, raw: raw)
+ end
+
+ let(:private_poll) do
+ raw = <<-RAW.strip_heredoc
+ [poll]
+ - A
+ - B
+ [/poll]
+ RAW
+
+ DiscoursePoll::PollsValidator.new(Fabricate(:post, raw: raw)).validate_polls
+ end
+
+ let(:public_poll) do
+ raw = <<-RAW.strip_heredoc
+ [poll public=true]
+ - A
+ - C
+ [/poll]
+ RAW
+
+ DiscoursePoll::PollsValidator.new(Fabricate(:post, raw: raw)).validate_polls
+ end
+
+ let(:user) { Fabricate(:user) }
+
+ before do
+ DiscoursePoll::Poll.vote(post.id, "poll", ["5c24fc1df56d764b550ceae1b9319125"], user.id)
+ post.reload
+ end
+
+ it "should retain voter_ids when options have been edited" do
+ described_class.update(post, public_poll)
+
+ polls = post.reload.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD]
+
+ expect(polls["poll"]["options"][0]["voter_ids"]).to eq([user.id])
+ expect(polls["poll"]["options"][1]["voter_ids"]).to eq([])
+ end
+
+ it "should delete voter_ids when poll is set to private" do
+ described_class.update(post, private_poll)
+
+ polls = post.reload.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD]
+
+ expect(post.reload.custom_fields[DiscoursePoll::POLLS_CUSTOM_FIELD])
+ .to eq(private_poll)
+
+ expect(polls["poll"]["options"][0]["voter_ids"]).to eq(nil)
+ expect(polls["poll"]["options"][1]["voter_ids"]).to eq(nil)
+ end
+ end
+
context "polls of type 'multiple'" do
let(:min_2_post) do
raw = <<-RAW.strip_heredoc