FEATURE: Allow users to remove their vote (#14459)
They can use the remove vote button or select the same option again for single choice polls. This commit refactor the plugin to properly organize code and make it easier to follow.
This commit is contained in:
parent
12856ab8c2
commit
6a143030f8
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DiscoursePoll::PollsController < ::ApplicationController
|
||||
requires_plugin DiscoursePoll::PLUGIN_NAME
|
||||
|
||||
before_action :ensure_logged_in, except: [:voters, :grouped_poll_results]
|
||||
|
||||
def vote
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
options = params.require(:options)
|
||||
|
||||
begin
|
||||
poll, options = DiscoursePoll::Poll.vote(current_user, post_id, poll_name, options)
|
||||
render json: { poll: poll, vote: options }
|
||||
rescue DiscoursePoll::Error => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def remove_vote
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
|
||||
begin
|
||||
poll = DiscoursePoll::Poll.remove_vote(current_user, post_id, poll_name)
|
||||
render json: { poll: poll }
|
||||
rescue DiscoursePoll::Error => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
status = params.require(:status)
|
||||
|
||||
begin
|
||||
poll = DiscoursePoll::Poll.toggle_status(current_user, post_id, poll_name, status)
|
||||
render json: { poll: poll }
|
||||
rescue DiscoursePoll::Error => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def voters
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
opts = params.permit(:limit, :page, :option_id)
|
||||
|
||||
raise Discourse::InvalidParameters.new(:post_id) if !Post.where(id: post_id).exists?
|
||||
|
||||
poll = Poll.find_by(post_id: post_id, name: poll_name)
|
||||
raise Discourse::InvalidParameters.new(:poll_name) if !poll&.can_see_voters?(current_user)
|
||||
|
||||
render json: { voters: DiscoursePoll::Poll.serialized_voters(poll, opts) }
|
||||
end
|
||||
|
||||
def grouped_poll_results
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
user_field_name = params.require(:user_field_name)
|
||||
|
||||
begin
|
||||
render json: {
|
||||
grouped_results: DiscoursePoll::Poll.grouped_poll_results(current_user, post_id, poll_name, user_field_name)
|
||||
}
|
||||
rescue DiscoursePoll::Error => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
end
|
|
@ -109,6 +109,7 @@ function initializePolls(api) {
|
|||
post: pollPost,
|
||||
poll,
|
||||
vote,
|
||||
hasSavedVote: vote.length > 0,
|
||||
titleHTML: titleElement && titleElement.outerHTML,
|
||||
groupableUserFields: (
|
||||
api.container.lookup("site-settings:main")
|
||||
|
|
|
@ -636,24 +636,44 @@ createWidget("discourse-poll-buttons", {
|
|||
})
|
||||
);
|
||||
} else {
|
||||
let showResultsButton;
|
||||
let infoText;
|
||||
|
||||
if (poll.results === "on_vote" && !attrs.hasVoted && !isMe) {
|
||||
contents.push(infoTextHtml(I18n.t("poll.results.vote.title")));
|
||||
infoText = infoTextHtml(I18n.t("poll.results.vote.title"));
|
||||
} else if (poll.results === "on_close" && !closed) {
|
||||
contents.push(infoTextHtml(I18n.t("poll.results.closed.title")));
|
||||
infoText = infoTextHtml(I18n.t("poll.results.closed.title"));
|
||||
} else if (poll.results === "staff_only" && !isStaff) {
|
||||
contents.push(infoTextHtml(I18n.t("poll.results.staff.title")));
|
||||
infoText = infoTextHtml(I18n.t("poll.results.staff.title"));
|
||||
} else {
|
||||
showResultsButton = this.attach("button", {
|
||||
className: "btn-default toggle-results",
|
||||
label: "poll.show-results.label",
|
||||
title: "poll.show-results.title",
|
||||
icon: "far-eye",
|
||||
action: "toggleResults",
|
||||
});
|
||||
}
|
||||
|
||||
if (showResultsButton) {
|
||||
contents.push(showResultsButton);
|
||||
}
|
||||
|
||||
if (attrs.hasSavedVote) {
|
||||
contents.push(
|
||||
this.attach("button", {
|
||||
className: "btn-default toggle-results",
|
||||
label: "poll.show-results.label",
|
||||
title: "poll.show-results.title",
|
||||
icon: "far-eye",
|
||||
disabled: poll.voters === 0,
|
||||
action: "toggleResults",
|
||||
className: "btn-default remove-vote",
|
||||
label: "poll.remove-vote.label",
|
||||
title: "poll.remove-vote.title",
|
||||
icon: "trash-alt",
|
||||
action: "removeVote",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (infoText) {
|
||||
contents.push(infoText);
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.groupableUserFields.length && poll.voters > 0) {
|
||||
|
@ -894,6 +914,28 @@ export default createWidget("discourse-poll", {
|
|||
this.state.showResults = !this.state.showResults;
|
||||
},
|
||||
|
||||
removeVote() {
|
||||
const { attrs, state } = this;
|
||||
state.loading = true;
|
||||
return ajax("/polls/vote", {
|
||||
type: "DELETE",
|
||||
data: {
|
||||
post_id: attrs.post.id,
|
||||
poll_name: attrs.poll.name,
|
||||
},
|
||||
})
|
||||
.then(({ poll }) => {
|
||||
attrs.poll.setProperties(poll);
|
||||
attrs.vote.length = 0;
|
||||
attrs.hasSavedVote = false;
|
||||
this.appEvents.trigger("poll:voted", poll, attrs.post, attrs.vote);
|
||||
})
|
||||
.catch((error) => popupAjaxError(error))
|
||||
.finally(() => {
|
||||
state.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
exportResults() {
|
||||
const { attrs } = this;
|
||||
const queryID = this.siteSettings.poll_export_data_explorer_query_id;
|
||||
|
@ -963,6 +1005,10 @@ export default createWidget("discourse-poll", {
|
|||
}
|
||||
|
||||
const { vote } = attrs;
|
||||
if (!this.isMultiple() && vote.length === 1 && vote[0] === option.id) {
|
||||
return this.removeVote();
|
||||
}
|
||||
|
||||
if (!this.isMultiple()) {
|
||||
vote.length = 0;
|
||||
}
|
||||
|
@ -994,6 +1040,7 @@ export default createWidget("discourse-poll", {
|
|||
},
|
||||
})
|
||||
.then(({ poll }) => {
|
||||
attrs.hasSavedVote = true;
|
||||
attrs.poll.setProperties(poll);
|
||||
this.appEvents.trigger("poll:voted", poll, attrs.post, attrs.vote);
|
||||
|
||||
|
|
|
@ -44,6 +44,10 @@ en:
|
|||
title: "Display the poll results"
|
||||
label: "Show results"
|
||||
|
||||
remove-vote:
|
||||
title: "Remove your vote"
|
||||
label: "Remove vote"
|
||||
|
||||
hide-results:
|
||||
title: "Back to your votes"
|
||||
label: "Show vote"
|
||||
|
|
|
@ -13,10 +13,10 @@ module Jobs
|
|||
end
|
||||
|
||||
DiscoursePoll::Poll.toggle_status(
|
||||
Discourse.system_user,
|
||||
args[:post_id],
|
||||
args[:poll_name],
|
||||
"closed",
|
||||
Discourse.system_user,
|
||||
false
|
||||
)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,338 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DiscoursePoll::Poll
|
||||
def self.vote(user, post_id, poll_name, options)
|
||||
serialized_poll = DiscoursePoll::Poll.change_vote(user, post_id, poll_name) do |poll|
|
||||
# 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) }
|
||||
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.requires_at_least_1_valid_option") if options.empty?
|
||||
|
||||
new_option_ids = poll.poll_options.each_with_object([]) do |option, obj|
|
||||
obj << option.id if options.include?(option.digest)
|
||||
end
|
||||
|
||||
old_option_ids = poll.poll_options.each_with_object([]) do |option, obj|
|
||||
if option.poll_votes.where(user_id: user.id).exists?
|
||||
obj << option.id
|
||||
end
|
||||
end
|
||||
|
||||
# remove non-selected votes
|
||||
PollVote
|
||||
.where(poll: poll, user: user)
|
||||
.where.not(poll_option_id: new_option_ids)
|
||||
.delete_all
|
||||
|
||||
# create missing votes
|
||||
(new_option_ids - old_option_ids).each do |option_id|
|
||||
PollVote.create!(poll: poll, user: user, poll_option_id: option_id)
|
||||
end
|
||||
end
|
||||
|
||||
[serialized_poll, options]
|
||||
end
|
||||
|
||||
def self.remove_vote(user, post_id, poll_name)
|
||||
DiscoursePoll::Poll.change_vote(user, post_id, poll_name) do |poll|
|
||||
PollVote.where(poll: poll, user: user).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
def self.toggle_status(user, post_id, poll_name, status, raise_errors = true)
|
||||
Poll.transaction do
|
||||
post = Post.find_by(id: post_id)
|
||||
guardian = Guardian.new(user)
|
||||
|
||||
# post must not be deleted
|
||||
if post.nil? || post.trashed?
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.post_is_deleted") if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
# topic must not be archived
|
||||
if post.topic&.archived
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.topic_must_be_open_to_toggle_status") if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
# either staff member or OP
|
||||
unless post.user_id == user&.id || user&.staff?
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.only_staff_or_op_can_toggle_status") if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
poll = Poll.find_by(post_id: post_id, name: poll_name)
|
||||
|
||||
if !poll
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
poll.status = status
|
||||
poll.save!
|
||||
|
||||
serialized_poll = PollSerializer.new(poll, root: false, scope: guardian).as_json
|
||||
payload = { post_id: post_id, polls: [serialized_poll] }
|
||||
|
||||
post.publish_message!("/polls/#{post.topic_id}", payload)
|
||||
|
||||
serialized_poll
|
||||
end
|
||||
end
|
||||
|
||||
def self.serialized_voters(poll, opts = {})
|
||||
limit = (opts["limit"] || 25).to_i
|
||||
limit = 0 if limit < 0
|
||||
limit = 50 if limit > 50
|
||||
|
||||
page = (opts["page"] || 1).to_i
|
||||
page = 1 if page < 1
|
||||
|
||||
offset = (page - 1) * limit
|
||||
|
||||
option_digest = opts["option_id"].to_s
|
||||
|
||||
if poll.number?
|
||||
user_ids = PollVote
|
||||
.where(poll: poll)
|
||||
.group(:user_id)
|
||||
.order("MIN(created_at)")
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.pluck(:user_id)
|
||||
|
||||
result = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash }
|
||||
elsif option_digest.present?
|
||||
poll_option = PollOption.find_by(poll: poll, digest: option_digest)
|
||||
|
||||
raise Discourse::InvalidParameters.new(:option_id) unless poll_option
|
||||
|
||||
user_ids = PollVote
|
||||
.where(poll: poll, poll_option: poll_option)
|
||||
.group(:user_id)
|
||||
.order("MIN(created_at)")
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.pluck(:user_id)
|
||||
|
||||
user_hashes = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash }
|
||||
|
||||
result = { option_digest => user_hashes }
|
||||
else
|
||||
votes = DB.query <<~SQL
|
||||
SELECT digest, user_id
|
||||
FROM (
|
||||
SELECT digest
|
||||
, user_id
|
||||
, ROW_NUMBER() OVER (PARTITION BY poll_option_id ORDER BY pv.created_at) AS row
|
||||
FROM poll_votes pv
|
||||
JOIN poll_options po ON pv.poll_option_id = po.id
|
||||
WHERE pv.poll_id = #{poll.id}
|
||||
AND po.poll_id = #{poll.id}
|
||||
) v
|
||||
WHERE row BETWEEN #{offset} AND #{offset + limit}
|
||||
SQL
|
||||
|
||||
user_ids = votes.map(&:user_id).uniq
|
||||
|
||||
user_hashes = User
|
||||
.where(id: user_ids)
|
||||
.map { |u| [u.id, UserNameSerializer.new(u).serializable_hash] }
|
||||
.to_h
|
||||
|
||||
result = {}
|
||||
votes.each do |v|
|
||||
result[v.digest] ||= []
|
||||
result[v.digest] << user_hashes[v.user_id]
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def self.transform_for_user_field_override(custom_user_field)
|
||||
existing_field = UserField.find_by(name: custom_user_field)
|
||||
existing_field ? "user_field_#{existing_field.id}" : custom_user_field
|
||||
end
|
||||
|
||||
def self.grouped_poll_results(user, post_id, poll_name, user_field_name)
|
||||
raise Discourse::InvalidParameters.new(:post_id) if !Post.where(id: post_id).exists?
|
||||
|
||||
poll = Poll.includes(:poll_options).includes(:poll_votes).find_by(post_id: post_id, name: poll_name)
|
||||
raise Discourse::InvalidParameters.new(:poll_name) unless poll
|
||||
|
||||
raise Discourse::InvalidParameters.new(:user_field_name) unless SiteSetting.poll_groupable_user_fields.split('|').include?(user_field_name)
|
||||
|
||||
poll_votes = poll.poll_votes
|
||||
|
||||
poll_options = {}
|
||||
poll.poll_options.each do |option|
|
||||
poll_options[option.id.to_s] = { html: option.html, digest: option.digest }
|
||||
end
|
||||
|
||||
user_ids = poll_votes.map(&:user_id).uniq
|
||||
user_fields = UserCustomField.where(user_id: user_ids, name: transform_for_user_field_override(user_field_name))
|
||||
|
||||
user_field_map = {}
|
||||
user_fields.each do |f|
|
||||
# Build hash, so we can quickly look up field values for each user.
|
||||
user_field_map[f.user_id] = f.value
|
||||
end
|
||||
|
||||
votes_with_field = poll_votes.map do |vote|
|
||||
v = vote.attributes
|
||||
v[:field_value] = user_field_map[vote.user_id]
|
||||
v
|
||||
end
|
||||
|
||||
chart_data = []
|
||||
votes_with_field.group_by { |vote| vote[:field_value] }.each do |field_answer, votes|
|
||||
grouped_selected_options = {}
|
||||
|
||||
# Create all the options with 0 votes. This ensures all the charts will have the same order of options, and same colors per option.
|
||||
poll_options.each do |id, option|
|
||||
grouped_selected_options[id] = {
|
||||
digest: option[:digest],
|
||||
html: option[:html],
|
||||
votes: 0
|
||||
}
|
||||
end
|
||||
|
||||
# Now go back and update the vote counts. Using hashes so we dont have n^2
|
||||
votes.group_by { |v| v["poll_option_id"] }.each do |option_id, votes_for_option|
|
||||
grouped_selected_options[option_id.to_s][:votes] = votes_for_option.length
|
||||
end
|
||||
|
||||
group_label = field_answer ? field_answer.titleize : I18n.t("poll.user_field.no_data")
|
||||
chart_data << { group: group_label, options: grouped_selected_options.values }
|
||||
end
|
||||
chart_data
|
||||
end
|
||||
|
||||
def self.schedule_jobs(post)
|
||||
Poll.where(post: post).find_each do |poll|
|
||||
job_args = {
|
||||
post_id: post.id,
|
||||
poll_name: poll.name
|
||||
}
|
||||
|
||||
Jobs.cancel_scheduled_job(:close_poll, job_args)
|
||||
|
||||
if poll.open? && poll.close_at && poll.close_at > Time.zone.now
|
||||
Jobs.enqueue_at(poll.close_at, :close_poll, job_args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.create!(post_id, poll)
|
||||
close_at = begin
|
||||
Time.zone.parse(poll["close"] || '')
|
||||
rescue ArgumentError
|
||||
end
|
||||
|
||||
created_poll = Poll.create!(
|
||||
post_id: post_id,
|
||||
name: poll["name"].presence || "poll",
|
||||
close_at: close_at,
|
||||
type: poll["type"].presence || "regular",
|
||||
status: poll["status"].presence || "open",
|
||||
visibility: poll["public"] == "true" ? "everyone" : "secret",
|
||||
title: poll["title"],
|
||||
results: poll["results"].presence || "always",
|
||||
min: poll["min"],
|
||||
max: poll["max"],
|
||||
step: poll["step"],
|
||||
chart_type: poll["charttype"] || "bar",
|
||||
groups: poll["groups"]
|
||||
)
|
||||
|
||||
poll["options"].each do |option|
|
||||
PollOption.create!(
|
||||
poll: created_poll,
|
||||
digest: option["id"].presence,
|
||||
html: option["html"].presence&.strip
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def self.extract(raw, topic_id, user_id = nil)
|
||||
# TODO: we should fix the callback mess so that the cooked version is available
|
||||
# in the validators instead of cooking twice
|
||||
cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id)
|
||||
|
||||
Nokogiri::HTML5(cooked).css("div.poll").map do |p|
|
||||
poll = { "options" => [], "name" => DiscoursePoll::DEFAULT_POLL_NAME }
|
||||
|
||||
# attributes
|
||||
p.attributes.values.each do |attribute|
|
||||
if attribute.name.start_with?(DiscoursePoll::DATA_PREFIX)
|
||||
poll[attribute.name[DiscoursePoll::DATA_PREFIX.length..-1]] = CGI.escapeHTML(attribute.value || "")
|
||||
end
|
||||
end
|
||||
|
||||
# options
|
||||
p.css("li[#{DiscoursePoll::DATA_PREFIX}option-id]").each do |o|
|
||||
option_id = o.attributes[DiscoursePoll::DATA_PREFIX + "option-id"].value.to_s
|
||||
poll["options"] << { "id" => option_id, "html" => o.inner_html.strip }
|
||||
end
|
||||
|
||||
# title
|
||||
title_element = p.css(".poll-title").first
|
||||
if title_element
|
||||
poll["title"] = title_element.inner_html.strip
|
||||
end
|
||||
|
||||
poll
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.change_vote(user, post_id, poll_name)
|
||||
Poll.transaction do
|
||||
post = Post.find_by(id: post_id)
|
||||
|
||||
# post must not be deleted
|
||||
if post.nil? || post.trashed?
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.post_is_deleted")
|
||||
end
|
||||
|
||||
# topic must not be archived
|
||||
if post.topic&.archived
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.topic_must_be_open_to_vote")
|
||||
end
|
||||
|
||||
# user must be allowed to post in topic
|
||||
guardian = Guardian.new(user)
|
||||
if !guardian.can_create_post?(post.topic)
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.user_cant_post_in_topic")
|
||||
end
|
||||
|
||||
poll = Poll.includes(:poll_options).find_by(post_id: post_id, name: poll_name)
|
||||
|
||||
raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) unless poll
|
||||
raise DiscoursePoll::Error.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 DiscoursePoll::Error.new I18n.t("js.poll.results.groups.title", groups: poll.groups)
|
||||
end
|
||||
end
|
||||
|
||||
yield(poll)
|
||||
|
||||
poll.reload
|
||||
|
||||
serialized_poll = PollSerializer.new(poll, root: false, scope: guardian).as_json
|
||||
payload = { post_id: post_id, polls: [serialized_poll] }
|
||||
|
||||
post.publish_message!("/polls/#{post.topic_id}", payload)
|
||||
|
||||
serialized_poll
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,35 +7,21 @@
|
|||
# url: https://github.com/discourse/discourse/tree/main/plugins/poll
|
||||
|
||||
register_asset "stylesheets/common/poll.scss"
|
||||
register_asset "stylesheets/common/poll-ui-builder.scss"
|
||||
register_asset "stylesheets/common/poll-breakdown.scss"
|
||||
register_asset "stylesheets/desktop/poll.scss", :desktop
|
||||
register_asset "stylesheets/desktop/poll-ui-builder.scss", :desktop
|
||||
register_asset "stylesheets/mobile/poll.scss", :mobile
|
||||
register_asset "stylesheets/common/poll-ui-builder.scss"
|
||||
register_asset "stylesheets/desktop/poll-ui-builder.scss", :desktop
|
||||
register_asset "stylesheets/common/poll-breakdown.scss"
|
||||
|
||||
register_svg_icon "far fa-check-square"
|
||||
|
||||
enabled_site_setting :poll_enabled
|
||||
hide_plugin if self.respond_to?(:hide_plugin)
|
||||
|
||||
PLUGIN_NAME ||= "discourse_poll"
|
||||
DATA_PREFIX ||= "data-poll-"
|
||||
hide_plugin
|
||||
|
||||
after_initialize do
|
||||
|
||||
[
|
||||
"../app/models/poll_vote",
|
||||
"../app/models/poll_option",
|
||||
"../app/models/poll",
|
||||
"../app/serializers/poll_option_serializer",
|
||||
"../app/serializers/poll_serializer",
|
||||
"../lib/polls_validator",
|
||||
"../lib/polls_updater",
|
||||
"../lib/post_validator",
|
||||
"../jobs/regular/close_poll",
|
||||
].each { |path| require File.expand_path(path, __FILE__) }
|
||||
|
||||
module ::DiscoursePoll
|
||||
PLUGIN_NAME ||= "discourse_poll"
|
||||
DATA_PREFIX ||= "data-poll-"
|
||||
HAS_POLLS ||= "has_polls"
|
||||
DEFAULT_POLL_NAME ||= "poll"
|
||||
|
||||
|
@ -43,434 +29,38 @@ after_initialize do
|
|||
engine_name PLUGIN_NAME
|
||||
isolate_namespace DiscoursePoll
|
||||
end
|
||||
|
||||
class Error < StandardError; end
|
||||
end
|
||||
|
||||
class DiscoursePoll::Poll
|
||||
class << self
|
||||
|
||||
def vote(post_id, poll_name, options, user)
|
||||
Poll.transaction do
|
||||
post = Post.find_by(id: post_id)
|
||||
|
||||
# post must not be deleted
|
||||
if post.nil? || post.trashed?
|
||||
raise StandardError.new I18n.t("poll.post_is_deleted")
|
||||
end
|
||||
|
||||
# topic must not be archived
|
||||
if post.topic&.archived
|
||||
raise StandardError.new I18n.t("poll.topic_must_be_open_to_vote")
|
||||
end
|
||||
|
||||
# user must be allowed to post in topic
|
||||
guardian = Guardian.new(user)
|
||||
if !guardian.can_create_post?(post.topic)
|
||||
raise StandardError.new I18n.t("poll.user_cant_post_in_topic")
|
||||
end
|
||||
|
||||
poll = Poll.includes(:poll_options).find_by(post_id: post_id, name: poll_name)
|
||||
|
||||
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", groups: 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) }
|
||||
|
||||
raise StandardError.new I18n.t("poll.requires_at_least_1_valid_option") if options.empty?
|
||||
|
||||
new_option_ids = poll.poll_options.each_with_object([]) do |option, obj|
|
||||
obj << option.id if options.include?(option.digest)
|
||||
end
|
||||
|
||||
old_option_ids = poll.poll_options.each_with_object([]) do |option, obj|
|
||||
if option.poll_votes.where(user_id: user.id).exists?
|
||||
obj << option.id
|
||||
end
|
||||
end
|
||||
|
||||
# remove non-selected votes
|
||||
PollVote
|
||||
.where(poll: poll, user: user)
|
||||
.where.not(poll_option_id: new_option_ids)
|
||||
.delete_all
|
||||
|
||||
# create missing votes
|
||||
(new_option_ids - old_option_ids).each do |option_id|
|
||||
PollVote.create!(poll: poll, user: user, poll_option_id: option_id)
|
||||
end
|
||||
|
||||
poll.reload
|
||||
|
||||
serialized_poll = PollSerializer.new(poll, root: false, scope: guardian).as_json
|
||||
payload = { post_id: post_id, polls: [serialized_poll] }
|
||||
|
||||
post.publish_message!("/polls/#{post.topic_id}", payload)
|
||||
|
||||
[serialized_poll, options]
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_status(post_id, poll_name, status, user, raise_errors = true)
|
||||
Poll.transaction do
|
||||
post = Post.find_by(id: post_id)
|
||||
guardian = Guardian.new(user)
|
||||
|
||||
# post must not be deleted
|
||||
if post.nil? || post.trashed?
|
||||
raise StandardError.new I18n.t("poll.post_is_deleted") if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
# topic must not be archived
|
||||
if post.topic&.archived
|
||||
raise StandardError.new I18n.t("poll.topic_must_be_open_to_toggle_status") if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
# either staff member or OP
|
||||
unless post.user_id == user&.id || user&.staff?
|
||||
raise StandardError.new I18n.t("poll.only_staff_or_op_can_toggle_status") if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
poll = Poll.find_by(post_id: post_id, name: poll_name)
|
||||
|
||||
if !poll
|
||||
raise StandardError.new I18n.t("poll.no_poll_with_this_name", name: poll_name) if raise_errors
|
||||
return
|
||||
end
|
||||
|
||||
poll.status = status
|
||||
poll.save!
|
||||
|
||||
serialized_poll = PollSerializer.new(poll, root: false, scope: guardian).as_json
|
||||
payload = { post_id: post_id, polls: [serialized_poll] }
|
||||
|
||||
post.publish_message!("/polls/#{post.topic_id}", payload)
|
||||
|
||||
serialized_poll
|
||||
end
|
||||
end
|
||||
|
||||
def serialized_voters(poll, opts = {})
|
||||
limit = (opts["limit"] || 25).to_i
|
||||
limit = 0 if limit < 0
|
||||
limit = 50 if limit > 50
|
||||
|
||||
page = (opts["page"] || 1).to_i
|
||||
page = 1 if page < 1
|
||||
|
||||
offset = (page - 1) * limit
|
||||
|
||||
option_digest = opts["option_id"].to_s
|
||||
|
||||
if poll.number?
|
||||
user_ids = PollVote
|
||||
.where(poll: poll)
|
||||
.group(:user_id)
|
||||
.order("MIN(created_at)")
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.pluck(:user_id)
|
||||
|
||||
result = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash }
|
||||
elsif option_digest.present?
|
||||
poll_option = PollOption.find_by(poll: poll, digest: option_digest)
|
||||
|
||||
raise Discourse::InvalidParameters.new(:option_id) unless poll_option
|
||||
|
||||
user_ids = PollVote
|
||||
.where(poll: poll, poll_option: poll_option)
|
||||
.group(:user_id)
|
||||
.order("MIN(created_at)")
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.pluck(:user_id)
|
||||
|
||||
user_hashes = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash }
|
||||
|
||||
result = { option_digest => user_hashes }
|
||||
else
|
||||
votes = DB.query <<~SQL
|
||||
SELECT digest, user_id
|
||||
FROM (
|
||||
SELECT digest
|
||||
, user_id
|
||||
, ROW_NUMBER() OVER (PARTITION BY poll_option_id ORDER BY pv.created_at) AS row
|
||||
FROM poll_votes pv
|
||||
JOIN poll_options po ON pv.poll_option_id = po.id
|
||||
WHERE pv.poll_id = #{poll.id}
|
||||
AND po.poll_id = #{poll.id}
|
||||
) v
|
||||
WHERE row BETWEEN #{offset} AND #{offset + limit}
|
||||
SQL
|
||||
|
||||
user_ids = votes.map(&:user_id).uniq
|
||||
|
||||
user_hashes = User
|
||||
.where(id: user_ids)
|
||||
.map { |u| [u.id, UserNameSerializer.new(u).serializable_hash] }
|
||||
.to_h
|
||||
|
||||
result = {}
|
||||
votes.each do |v|
|
||||
result[v.digest] ||= []
|
||||
result[v.digest] << user_hashes[v.user_id]
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def voters(post_id, poll_name, user, opts = {})
|
||||
post = Post.find_by(id: post_id)
|
||||
raise Discourse::InvalidParameters.new(:post_id) unless post
|
||||
|
||||
poll = Poll.find_by(post_id: post_id, name: poll_name)
|
||||
raise Discourse::InvalidParameters.new(:poll_name) unless poll&.can_see_voters?(user)
|
||||
|
||||
serialized_voters(poll, opts)
|
||||
end
|
||||
|
||||
def transform_for_user_field_override(custom_user_field)
|
||||
existing_field = UserField.find_by(name: custom_user_field)
|
||||
existing_field ? "user_field_#{existing_field.id}" : custom_user_field
|
||||
end
|
||||
|
||||
def grouped_poll_results(post_id, poll_name, user_field_name, user)
|
||||
post = Post.find_by(id: post_id)
|
||||
raise Discourse::InvalidParameters.new(:post_id) unless post
|
||||
|
||||
poll = Poll.includes(:poll_options).includes(:poll_votes).find_by(post_id: post_id, name: poll_name)
|
||||
raise Discourse::InvalidParameters.new(:poll_name) unless poll
|
||||
|
||||
raise Discourse::InvalidParameters.new(:user_field_name) unless SiteSetting.poll_groupable_user_fields.split('|').include?(user_field_name)
|
||||
|
||||
poll_votes = poll.poll_votes
|
||||
|
||||
poll_options = {}
|
||||
poll.poll_options.each do |option|
|
||||
poll_options[option.id.to_s] = { html: option.html, digest: option.digest }
|
||||
end
|
||||
|
||||
user_ids = poll_votes.map(&:user_id).uniq
|
||||
user_fields = UserCustomField.where(user_id: user_ids, name: transform_for_user_field_override(user_field_name))
|
||||
|
||||
user_field_map = {}
|
||||
user_fields.each do |f|
|
||||
# Build hash, so we can quickly look up field values for each user.
|
||||
user_field_map[f.user_id] = f.value
|
||||
end
|
||||
|
||||
votes_with_field = poll_votes.map do |vote|
|
||||
v = vote.attributes
|
||||
v[:field_value] = user_field_map[vote.user_id]
|
||||
v
|
||||
end
|
||||
|
||||
chart_data = []
|
||||
votes_with_field.group_by { |vote| vote[:field_value] }.each do |field_answer, votes|
|
||||
grouped_selected_options = {}
|
||||
|
||||
# Create all the options with 0 votes. This ensures all the charts will have the same order of options, and same colors per option.
|
||||
poll_options.each do |id, option|
|
||||
grouped_selected_options[id] = {
|
||||
digest: option[:digest],
|
||||
html: option[:html],
|
||||
votes: 0
|
||||
}
|
||||
end
|
||||
|
||||
# Now go back and update the vote counts. Using hashes so we dont have n^2
|
||||
votes.group_by { |v| v["poll_option_id"] }.each do |option_id, votes_for_option|
|
||||
grouped_selected_options[option_id.to_s][:votes] = votes_for_option.length
|
||||
end
|
||||
|
||||
group_label = field_answer ? field_answer.titleize : I18n.t("poll.user_field.no_data")
|
||||
chart_data << { group: group_label, options: grouped_selected_options.values }
|
||||
end
|
||||
chart_data
|
||||
end
|
||||
|
||||
def schedule_jobs(post)
|
||||
Poll.where(post: post).find_each do |poll|
|
||||
job_args = {
|
||||
post_id: post.id,
|
||||
poll_name: poll.name
|
||||
}
|
||||
|
||||
Jobs.cancel_scheduled_job(:close_poll, job_args)
|
||||
|
||||
if poll.open? && poll.close_at && poll.close_at > Time.zone.now
|
||||
Jobs.enqueue_at(poll.close_at, :close_poll, job_args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create!(post_id, poll)
|
||||
close_at = begin
|
||||
Time.zone.parse(poll["close"] || '')
|
||||
rescue ArgumentError
|
||||
end
|
||||
|
||||
created_poll = Poll.create!(
|
||||
post_id: post_id,
|
||||
name: poll["name"].presence || "poll",
|
||||
close_at: close_at,
|
||||
type: poll["type"].presence || "regular",
|
||||
status: poll["status"].presence || "open",
|
||||
visibility: poll["public"] == "true" ? "everyone" : "secret",
|
||||
title: poll["title"],
|
||||
results: poll["results"].presence || "always",
|
||||
min: poll["min"],
|
||||
max: poll["max"],
|
||||
step: poll["step"],
|
||||
chart_type: poll["charttype"] || "bar",
|
||||
groups: poll["groups"]
|
||||
)
|
||||
|
||||
poll["options"].each do |option|
|
||||
PollOption.create!(
|
||||
poll: created_poll,
|
||||
digest: option["id"].presence,
|
||||
html: option["html"].presence&.strip
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def extract(raw, topic_id, user_id = nil)
|
||||
# TODO: we should fix the callback mess so that the cooked version is available
|
||||
# in the validators instead of cooking twice
|
||||
cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id)
|
||||
|
||||
Nokogiri::HTML5(cooked).css("div.poll").map do |p|
|
||||
poll = { "options" => [], "name" => DiscoursePoll::DEFAULT_POLL_NAME }
|
||||
|
||||
# attributes
|
||||
p.attributes.values.each do |attribute|
|
||||
if attribute.name.start_with?(DATA_PREFIX)
|
||||
poll[attribute.name[DATA_PREFIX.length..-1]] = CGI.escapeHTML(attribute.value || "")
|
||||
end
|
||||
end
|
||||
|
||||
# options
|
||||
p.css("li[#{DATA_PREFIX}option-id]").each do |o|
|
||||
option_id = o.attributes[DATA_PREFIX + "option-id"].value.to_s
|
||||
poll["options"] << { "id" => option_id, "html" => o.inner_html.strip }
|
||||
end
|
||||
|
||||
# title
|
||||
title_element = p.css(".poll-title").first
|
||||
if title_element
|
||||
poll["title"] = title_element.inner_html.strip
|
||||
end
|
||||
|
||||
poll
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class DiscoursePoll::PollsController < ::ApplicationController
|
||||
requires_plugin PLUGIN_NAME
|
||||
|
||||
before_action :ensure_logged_in, except: [:voters, :grouped_poll_results]
|
||||
|
||||
def vote
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
options = params.require(:options)
|
||||
|
||||
begin
|
||||
poll, options = DiscoursePoll::Poll.vote(post_id, poll_name, options, current_user)
|
||||
render json: { poll: poll, vote: options }
|
||||
rescue StandardError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def current_user_voted
|
||||
poll = Poll.includes(:post).find_by(id: params[:id])
|
||||
raise Discourse::NotFound.new(:id) if poll.nil?
|
||||
|
||||
can_see_poll = Guardian.new(current_user).can_see_post?(poll.post)
|
||||
raise Discourse::NotFound.new(:id) if !can_see_poll
|
||||
|
||||
presence = PollVote.where(poll: poll, user: current_user).exists?
|
||||
render json: { voted: presence }
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
status = params.require(:status)
|
||||
|
||||
begin
|
||||
poll = DiscoursePoll::Poll.toggle_status(post_id, poll_name, status, current_user)
|
||||
render json: { poll: poll }
|
||||
rescue StandardError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def voters
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
|
||||
opts = params.permit(:limit, :page, :option_id)
|
||||
|
||||
begin
|
||||
render json: { voters: DiscoursePoll::Poll.voters(post_id, poll_name, current_user, opts) }
|
||||
rescue StandardError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def grouped_poll_results
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
user_field_name = params.require(:user_field_name)
|
||||
|
||||
begin
|
||||
render json: {
|
||||
grouped_results: DiscoursePoll::Poll.grouped_poll_results(post_id, poll_name, user_field_name, current_user)
|
||||
}
|
||||
rescue StandardError => e
|
||||
render_json_error e.message
|
||||
end
|
||||
end
|
||||
|
||||
def groupable_user_fields
|
||||
render json: {
|
||||
fields: SiteSetting.poll_groupable_user_fields.split('|').map do |field|
|
||||
{ name: field.humanize.capitalize, value: field }
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
require_relative "app/controllers/polls_controller.rb"
|
||||
require_relative "app/models/poll_option.rb"
|
||||
require_relative "app/models/poll_vote.rb"
|
||||
require_relative "app/models/poll.rb"
|
||||
require_relative "app/serializers/poll_option_serializer.rb"
|
||||
require_relative "app/serializers/poll_serializer.rb"
|
||||
require_relative "jobs/regular/close_poll.rb"
|
||||
require_relative "lib/poll.rb"
|
||||
require_relative "lib/polls_updater.rb"
|
||||
require_relative "lib/polls_validator.rb"
|
||||
require_relative "lib/post_validator.rb"
|
||||
|
||||
DiscoursePoll::Engine.routes.draw do
|
||||
put "/vote" => "polls#vote"
|
||||
delete "/vote" => "polls#remove_vote"
|
||||
put "/toggle_status" => "polls#toggle_status"
|
||||
get "/voters" => 'polls#voters'
|
||||
get "/grouped_poll_results" => 'polls#grouped_poll_results'
|
||||
get "/groupable_user_fields" => 'polls#groupable_user_fields'
|
||||
get "/:id/votes/current_user_voted" => "polls#current_user_voted"
|
||||
end
|
||||
|
||||
Discourse::Application.routes.append do
|
||||
mount ::DiscoursePoll::Engine, at: "/polls"
|
||||
end
|
||||
|
||||
allow_new_queued_post_payload_attribute("is_poll")
|
||||
register_post_custom_field_type(DiscoursePoll::HAS_POLLS, :boolean)
|
||||
topic_view_post_custom_fields_allowlister { [DiscoursePoll::HAS_POLLS] }
|
||||
|
||||
reloadable_patch do
|
||||
Post.class_eval do
|
||||
attr_accessor :extracted_polls
|
||||
|
@ -519,8 +109,6 @@ after_initialize do
|
|||
true
|
||||
end
|
||||
|
||||
allow_new_queued_post_payload_attribute("is_poll")
|
||||
|
||||
NewPostManager.add_handler(1) do |manager|
|
||||
post = Post.new(raw: manager.args[:raw])
|
||||
|
||||
|
@ -581,10 +169,6 @@ after_initialize do
|
|||
PollVote.where(user_id: source_user.id).update_all(user_id: target_user.id)
|
||||
end
|
||||
|
||||
register_post_custom_field_type(DiscoursePoll::HAS_POLLS, :boolean)
|
||||
|
||||
topic_view_post_custom_fields_allowlister { [DiscoursePoll::HAS_POLLS] }
|
||||
|
||||
add_to_class(:topic_view, :polls) do
|
||||
@polls ||= begin
|
||||
polls = {}
|
||||
|
|
|
@ -13,7 +13,6 @@ describe ::DiscoursePoll::PollsController do
|
|||
let(:public_poll_on_close) { Fabricate(:post, topic: topic, user: user, raw: "[poll public=true results=on_close]\n- A\n- B\n[/poll]") }
|
||||
|
||||
describe "#vote" do
|
||||
|
||||
it "works" do
|
||||
channel = "/polls/#{poll.topic_id}"
|
||||
|
||||
|
@ -118,6 +117,24 @@ describe ::DiscoursePoll::PollsController do
|
|||
expect(json["poll"]["options"][1]["votes"]).to eq(1)
|
||||
end
|
||||
|
||||
it "supports removing votes" do
|
||||
put :vote, params: {
|
||||
post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"]
|
||||
}, format: :json
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
delete :remove_vote, params: {
|
||||
post_id: poll.id, poll_name: "poll"
|
||||
}, format: :json
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
json = response.parsed_body
|
||||
expect(json["poll"]["voters"]).to eq(0)
|
||||
expect(json["poll"]["options"][0]["votes"]).to eq(0)
|
||||
expect(json["poll"]["options"][1]["votes"]).to eq(0)
|
||||
end
|
||||
|
||||
it "works on closed topics" do
|
||||
topic.update_attribute(:closed, true)
|
||||
|
||||
|
@ -218,7 +235,6 @@ describe ::DiscoursePoll::PollsController do
|
|||
end
|
||||
|
||||
describe "#toggle_status" do
|
||||
|
||||
it "works for OP" do
|
||||
channel = "/polls/#{poll.topic_id}"
|
||||
|
||||
|
@ -264,11 +280,9 @@ describe ::DiscoursePoll::PollsController do
|
|||
json = response.parsed_body
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.post_is_deleted"))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#voters" do
|
||||
|
||||
let(:first) { "5c24fc1df56d764b550ceae1b9319125" }
|
||||
let(:second) { "e89dec30bbd9bf50fabf6a05b4324edf" }
|
||||
|
||||
|
@ -333,7 +347,7 @@ describe ::DiscoursePoll::PollsController do
|
|||
poll_name: "poll", post_id: public_poll_on_vote.id
|
||||
}, format: :json
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(response.status).to eq(400)
|
||||
|
||||
put :vote, params: {
|
||||
post_id: public_poll_on_vote.id, poll_name: "poll", options: [second]
|
||||
|
@ -364,7 +378,7 @@ describe ::DiscoursePoll::PollsController do
|
|||
poll_name: "poll", post_id: public_poll_on_close.id
|
||||
}, format: :json
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(response.status).to eq(400)
|
||||
|
||||
put :toggle_status, params: {
|
||||
post_id: public_poll_on_close.id, poll_name: "poll", status: "closed"
|
||||
|
@ -382,51 +396,5 @@ describe ::DiscoursePoll::PollsController do
|
|||
|
||||
expect(json["voters"][first].size).to eq(1)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '#current_user_voted' do
|
||||
let(:logged_user) { Fabricate(:user) }
|
||||
let(:post_with_poll) { Fabricate(:post, raw: "[poll]\n- A\n- B\n[/poll]") }
|
||||
|
||||
before { log_in_user(logged_user) }
|
||||
|
||||
it 'returns true if the logged user already voted' do
|
||||
poll = post_with_poll.polls.last
|
||||
PollVote.create!(poll: poll, user: logged_user)
|
||||
|
||||
get :current_user_voted, params: { id: poll.id }, format: :json
|
||||
parsed_body = JSON.parse(response.body)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(parsed_body['voted']).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns a 404 if there is no poll' do
|
||||
unknown_poll_id = 999999
|
||||
|
||||
get :current_user_voted, params: { id: unknown_poll_id }, format: :json
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "returns a 404 if the user doesn't have access to the poll" do
|
||||
pm_with_poll = Fabricate(:private_message_post, raw: "[poll]\n- A\n- B\n[/poll]")
|
||||
poll = pm_with_poll.polls.last
|
||||
|
||||
get :current_user_voted, params: { id: poll.id }, format: :json
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
|
||||
it "returns false if the user didn't vote yet" do
|
||||
poll = post_with_poll.polls.last
|
||||
|
||||
get :current_user_voted, params: { id: poll.id }, format: :json
|
||||
parsed_body = JSON.parse(response.body)
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(parsed_body['voted']).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -185,7 +185,7 @@ describe PostsController do
|
|||
end
|
||||
|
||||
it "resets the votes" do
|
||||
DiscoursePoll::Poll.vote(post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"], user)
|
||||
DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"])
|
||||
|
||||
put :update, params: {
|
||||
id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" }
|
||||
|
@ -244,7 +244,7 @@ describe PostsController do
|
|||
describe "with at least one vote" do
|
||||
|
||||
before do
|
||||
DiscoursePoll::Poll.vote(post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"], user)
|
||||
DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"])
|
||||
end
|
||||
|
||||
it "cannot change the options" do
|
||||
|
|
|
@ -11,10 +11,10 @@ describe "DiscoursePoll endpoints" do
|
|||
|
||||
it "should return the right response" do
|
||||
DiscoursePoll::Poll.vote(
|
||||
user,
|
||||
post.id,
|
||||
DiscoursePoll::DEFAULT_POLL_NAME,
|
||||
[option_a],
|
||||
user
|
||||
[option_a]
|
||||
)
|
||||
|
||||
get "/polls/voters.json", params: {
|
||||
|
@ -34,10 +34,10 @@ describe "DiscoursePoll endpoints" do
|
|||
|
||||
it 'should return the right response for a single option' do
|
||||
DiscoursePoll::Poll.vote(
|
||||
user,
|
||||
post.id,
|
||||
DiscoursePoll::DEFAULT_POLL_NAME,
|
||||
[option_a, option_b],
|
||||
user
|
||||
[option_a, option_b]
|
||||
)
|
||||
|
||||
get "/polls/voters.json", params: {
|
||||
|
@ -72,7 +72,7 @@ describe "DiscoursePoll endpoints" do
|
|||
post_id: -1,
|
||||
poll_name: DiscoursePoll::DEFAULT_POLL_NAME
|
||||
}
|
||||
expect(response.status).to eq(422)
|
||||
expect(response.status).to eq(400)
|
||||
expect(response.body).to include('post_id')
|
||||
end
|
||||
end
|
||||
|
@ -87,7 +87,7 @@ describe "DiscoursePoll endpoints" do
|
|||
describe 'when poll_name is not valid' do
|
||||
it 'should raise the right error' do
|
||||
get "/polls/voters.json", params: { post_id: post.id, poll_name: 'wrongpoll' }
|
||||
expect(response.status).to eq(422)
|
||||
expect(response.status).to eq(400)
|
||||
expect(response.body).to include('poll_name')
|
||||
end
|
||||
end
|
||||
|
@ -99,10 +99,10 @@ describe "DiscoursePoll endpoints" do
|
|||
post
|
||||
|
||||
DiscoursePoll::Poll.vote(
|
||||
user,
|
||||
post.id,
|
||||
DiscoursePoll::DEFAULT_POLL_NAME,
|
||||
["4d8a15e3cc35750f016ce15a43937620"],
|
||||
user
|
||||
["4d8a15e3cc35750f016ce15a43937620"]
|
||||
)
|
||||
|
||||
get "/polls/voters.json", params: {
|
||||
|
@ -137,20 +137,20 @@ describe "DiscoursePoll endpoints" do
|
|||
}
|
||||
[user1, user2, user3].each_with_index do |user, index|
|
||||
DiscoursePoll::Poll.vote(
|
||||
user,
|
||||
post.id,
|
||||
DiscoursePoll::DEFAULT_POLL_NAME,
|
||||
[user_votes["user_#{index}".to_sym]],
|
||||
user
|
||||
[user_votes["user_#{index}".to_sym]]
|
||||
)
|
||||
UserCustomField.create(user_id: user.id, name: "something", value: "value#{index}")
|
||||
end
|
||||
|
||||
# Add another user to one of the fields to prove it groups users properly
|
||||
DiscoursePoll::Poll.vote(
|
||||
user4,
|
||||
post.id,
|
||||
DiscoursePoll::DEFAULT_POLL_NAME,
|
||||
[option_a, option_b],
|
||||
user4
|
||||
[option_a, option_b]
|
||||
)
|
||||
UserCustomField.create(user_id: user4.id, name: "something", value: "value1")
|
||||
end
|
||||
|
@ -182,7 +182,7 @@ describe "DiscoursePoll endpoints" do
|
|||
user_field_name: "something"
|
||||
}
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(response.status).to eq(400)
|
||||
expect(response.body).to include('user_field_name')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -78,8 +78,8 @@ describe DiscoursePoll::PollsUpdater do
|
|||
let(:post) { Fabricate(:post, raw: raw) }
|
||||
|
||||
it "works if poll is closed and unmodified" do
|
||||
DiscoursePoll::Poll.vote(post.id, "poll", ["e55de753c08b93d04d677ce05e942d3c"], post.user)
|
||||
DiscoursePoll::Poll.toggle_status(post.id, "poll", "closed", post.user)
|
||||
DiscoursePoll::Poll.vote(post.user, post.id, "poll", ["e55de753c08b93d04d677ce05e942d3c"])
|
||||
DiscoursePoll::Poll.toggle_status(post.user, post.id, "poll", "closed")
|
||||
|
||||
freeze_time (SiteSetting.poll_edit_window_mins + 1).minutes.from_now
|
||||
update(post, DiscoursePoll::PollsValidator.new(post).validate_polls)
|
||||
|
@ -159,7 +159,7 @@ describe DiscoursePoll::PollsUpdater do
|
|||
|
||||
before do
|
||||
expect {
|
||||
DiscoursePoll::Poll.vote(post.id, "poll", [polls["poll"]["options"][0]["id"]], user)
|
||||
DiscoursePoll::Poll.vote(user, post.id, "poll", [polls["poll"]["options"][0]["id"]])
|
||||
}.to change { PollVote.count }.by(1)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
acceptance,
|
||||
count,
|
||||
publishToMessageBus,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { test } from "qunit";
|
||||
|
@ -558,6 +559,8 @@ acceptance("Poll results", function (needs) {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.delete("/polls/vote", () => helper.response({ success: "OK" }));
|
||||
});
|
||||
|
||||
test("can load more voters", async function (assert) {
|
||||
|
@ -643,4 +646,17 @@ acceptance("Poll results", function (needs) {
|
|||
0
|
||||
);
|
||||
});
|
||||
|
||||
test("can unvote", async function (assert) {
|
||||
await visit("/t/-/load-more-poll-voters");
|
||||
await click(".toggle-results");
|
||||
|
||||
assert.equal(count(".poll-container .d-icon-circle"), 1);
|
||||
assert.equal(count(".poll-container .d-icon-far-circle"), 1);
|
||||
|
||||
await click(".remove-vote");
|
||||
|
||||
assert.equal(count(".poll-container .d-icon-circle"), 0);
|
||||
assert.equal(count(".poll-container .d-icon-far-circle"), 2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -75,7 +75,6 @@ discourseModule(
|
|||
],
|
||||
voters: 1,
|
||||
chart_type: "bar",
|
||||
groups: "foo",
|
||||
},
|
||||
vote: ["1f972d1df351de3ce35a787c89faad29"],
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue