mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 09:36:19 -05:00
FEATURE: Add public type to polls.
This commit is contained in:
parent
7aac5baeed
commit
8ecde35df3
17 changed files with 322 additions and 30 deletions
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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"),
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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(() => {
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
{{{averageRating}}}
|
||||
|
||||
{{#if poll.public}}
|
||||
{{poll-results-number-voters poll=poll pollsVoters=pollsVoters}}
|
||||
{{/if}}
|
||||
|
|
|
@ -9,5 +9,9 @@
|
|||
<div class="bar-back">
|
||||
<div class="bar" style={{option.style}}></div>
|
||||
</div>
|
||||
|
||||
{{#if poll.public}}
|
||||
{{poll-results-standard-voters option=option pollsVoters=pollsVoters}}
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/each}}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<div class="poll-voters">
|
||||
{{#each users as |user|}}
|
||||
<li>
|
||||
<a data-user-card={{unbound user.username}}>
|
||||
{{avatar user imageSize="tiny"}}
|
||||
</a>
|
||||
</li>
|
||||
{{/each}}
|
||||
|
||||
<div class="poll-voters-toggle-expand">
|
||||
{{#if showMore}}
|
||||
{{#if isExpanded}}
|
||||
<a {{action "toggleExpand"}}>{{fa-icon "chevron-up"}}</a>
|
||||
{{else}}
|
||||
<a {{action "toggleExpand"}}>{{fa-icon "chevron-down"}}</a>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
|
@ -2,9 +2,9 @@
|
|||
<div class="poll-container">
|
||||
{{#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}}
|
||||
<ul>
|
||||
|
|
|
@ -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]));
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
|
@ -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")
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue