mirror of
https://github.com/discourse/discourse.git
synced 2024-12-02 23:33:54 +08:00
fcaefc9f2f
When merging users, polls may error out if the source and target users have both voted on the same poll before. 😢
There is no constraint on the `poll_votes` table either to support this. Ideally a composite primary key can be used `(poll_id, user_id)`, but alas there is no support yet, which is probably why it wasn't created in the first place.
This fix ensures that merging is successful by only keeping the target poll votes if duplicates exist.
This fix also runs a migration on older poll votes where failed merges would have caused a single user to have voted twice on a single poll. e.g. this weird edge case
256 lines
6.9 KiB
Ruby
256 lines
6.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# name: poll
|
|
# about: Official poll plugin for Discourse
|
|
# version: 1.0
|
|
# authors: Vikhyat Korrapati (vikhyat), Régis Hanol (zogstrip)
|
|
# url: https://github.com/discourse/discourse/tree/main/plugins/poll
|
|
|
|
register_asset "stylesheets/common/poll.scss"
|
|
register_asset "stylesheets/desktop/poll.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
|
|
|
|
after_initialize do
|
|
module ::DiscoursePoll
|
|
PLUGIN_NAME ||= "poll"
|
|
DATA_PREFIX ||= "data-poll-"
|
|
HAS_POLLS ||= "has_polls"
|
|
DEFAULT_POLL_NAME ||= "poll"
|
|
|
|
class Engine < ::Rails::Engine
|
|
engine_name PLUGIN_NAME
|
|
isolate_namespace DiscoursePoll
|
|
end
|
|
|
|
class Error < StandardError
|
|
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"
|
|
end
|
|
|
|
Discourse::Application.routes.append { mount ::DiscoursePoll::Engine, at: "/polls" }
|
|
|
|
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
|
|
|
|
has_many :polls, dependent: :destroy
|
|
|
|
after_save do
|
|
polls = self.extracted_polls
|
|
self.extracted_polls = nil
|
|
next if polls.blank? || !polls.is_a?(Hash)
|
|
post = self
|
|
|
|
Poll.transaction do
|
|
polls.values.each { |poll| DiscoursePoll::Poll.create!(post.id, poll) }
|
|
post.custom_fields[DiscoursePoll::HAS_POLLS] = true
|
|
post.save_custom_fields(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
User.class_eval { has_many :poll_votes, dependent: :delete_all }
|
|
end
|
|
|
|
validate(:post, :validate_polls) do |force = nil|
|
|
return unless self.raw_changed? || force
|
|
|
|
validator = DiscoursePoll::PollsValidator.new(self)
|
|
return unless (polls = validator.validate_polls)
|
|
|
|
if polls.present?
|
|
validator = DiscoursePoll::PostValidator.new(self)
|
|
return unless validator.validate_post
|
|
end
|
|
|
|
# are we updating a post?
|
|
if self.id.present?
|
|
DiscoursePoll::PollsUpdater.update(self, polls)
|
|
else
|
|
self.extracted_polls = polls
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
NewPostManager.add_handler(1) do |manager|
|
|
post = Post.new(raw: manager.args[:raw])
|
|
|
|
if !DiscoursePoll::PollsValidator.new(post).validate_polls
|
|
result = NewPostResult.new(:poll, false)
|
|
|
|
post.errors.full_messages.each { |message| result.add_error(message) }
|
|
|
|
result
|
|
else
|
|
manager.args["is_poll"] = true
|
|
nil
|
|
end
|
|
end
|
|
|
|
on(:approved_post) do |queued_post, created_post|
|
|
created_post.validate_polls(true) if queued_post.payload["is_poll"]
|
|
end
|
|
|
|
on(:reduce_cooked) do |fragment, post|
|
|
if post.nil? || post.trashed?
|
|
fragment.css(".poll, [data-poll-name]").each(&:remove)
|
|
else
|
|
post_url = post.full_url
|
|
fragment
|
|
.css(".poll, [data-poll-name]")
|
|
.each do |poll|
|
|
poll.replace "<p><a href='#{post_url}'>#{I18n.t("poll.email.link_to_poll")}</a></p>"
|
|
end
|
|
end
|
|
end
|
|
|
|
on(:reduce_excerpt) do |doc, options|
|
|
post = options[:post]
|
|
|
|
replacement =
|
|
(
|
|
if post&.url.present?
|
|
"<a href='#{UrlHelper.normalized_encode(post.url)}'>#{I18n.t("poll.poll")}</a>"
|
|
else
|
|
I18n.t("poll.poll")
|
|
end
|
|
)
|
|
|
|
doc.css("div.poll").each { |poll| poll.replace(replacement) }
|
|
end
|
|
|
|
on(:post_created) do |post, _opts, user|
|
|
guardian = Guardian.new(user)
|
|
DiscoursePoll::Poll.schedule_jobs(post)
|
|
|
|
next if post.is_first_post?
|
|
next if post.custom_fields[DiscoursePoll::HAS_POLLS].blank?
|
|
|
|
polls =
|
|
ActiveModel::ArraySerializer.new(
|
|
post.polls,
|
|
each_serializer: PollSerializer,
|
|
root: false,
|
|
scope: guardian,
|
|
).as_json
|
|
post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls)
|
|
end
|
|
|
|
on(:merging_users) do |source_user, target_user|
|
|
DB.exec(<<-SQL, source_user_id: source_user.id, target_user_id: target_user.id)
|
|
DELETE FROM poll_votes
|
|
WHERE poll_id IN (
|
|
SELECT poll_id
|
|
FROM poll_votes
|
|
WHERE user_id = :source_user_id
|
|
INTERSECT
|
|
SELECT poll_id
|
|
FROM poll_votes
|
|
WHERE user_id = :target_user_id
|
|
) AND user_id = :source_user_id;
|
|
|
|
UPDATE poll_votes
|
|
SET user_id = :target_user_id
|
|
WHERE user_id = :source_user_id;
|
|
SQL
|
|
end
|
|
|
|
add_to_class(:topic_view, :polls) do
|
|
@polls ||=
|
|
begin
|
|
polls = {}
|
|
|
|
post_with_polls =
|
|
@post_custom_fields.each_with_object([]) do |fields, obj|
|
|
obj << fields[0] if fields[1][DiscoursePoll::HAS_POLLS]
|
|
end
|
|
|
|
if post_with_polls.present?
|
|
Poll
|
|
.where(post_id: post_with_polls)
|
|
.each do |p|
|
|
polls[p.post_id] ||= []
|
|
polls[p.post_id] << p
|
|
end
|
|
end
|
|
|
|
polls
|
|
end
|
|
end
|
|
|
|
add_to_class(PostSerializer, :preloaded_polls) do
|
|
@preloaded_polls ||=
|
|
if @topic_view.present?
|
|
@topic_view.polls[object.id]
|
|
else
|
|
Poll.includes(:poll_options).where(post: object)
|
|
end
|
|
end
|
|
|
|
add_to_serializer(:post, :polls, include_condition: -> { preloaded_polls.present? }) do
|
|
preloaded_polls.map { |p| PollSerializer.new(p, root: false, scope: self.scope) }
|
|
end
|
|
|
|
add_to_serializer(
|
|
:post,
|
|
:polls_votes,
|
|
include_condition: -> do
|
|
scope.user&.id.present? && preloaded_polls.present? &&
|
|
preloaded_polls.any? { |p| p.has_voted?(scope.user) }
|
|
end,
|
|
) do
|
|
preloaded_polls
|
|
.map do |poll|
|
|
user_poll_votes =
|
|
poll
|
|
.poll_votes
|
|
.where(user_id: scope.user.id)
|
|
.joins(:poll_option)
|
|
.pluck("poll_options.digest")
|
|
|
|
[poll.name, user_poll_votes]
|
|
end
|
|
.to_h
|
|
end
|
|
|
|
register_search_advanced_filter(/in:polls/) do |posts, match|
|
|
if SiteSetting.poll_enabled
|
|
posts.joins(:polls)
|
|
else
|
|
posts
|
|
end
|
|
end
|
|
end
|