mirror of
https://github.com/discourse/discourse.git
synced 2026-08-14 13:58:53 +08:00
Previously, anonymous users clicking event RSVP buttons hit a dead-end with no signup prompt, and other engagement actions (Like, React, Vote) opened the login modal but lost the click on the way to authentication. This change captures the intent in a short-lived signed cookie via `POST /anonymous-action` and replays it through each action's existing service when `CurrentUser#log_on_user` completes, so anonymous engagement converts to signups with the original action carried through. Ref - t/184288 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
278 lines
8.3 KiB
Ruby
Vendored
278 lines
8.3 KiB
Ruby
Vendored
# 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/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-square-check"
|
|
|
|
enabled_site_setting :poll_enabled
|
|
|
|
module ::DiscoursePoll
|
|
PLUGIN_NAME = "poll"
|
|
end
|
|
|
|
require_relative "lib/poll/engine"
|
|
|
|
after_initialize do
|
|
require_relative "app/controllers/discourse_poll/polls_controller"
|
|
require_relative "app/models/poll_option"
|
|
require_relative "app/models/poll_vote"
|
|
require_relative "app/models/poll"
|
|
require_relative "app/serializers/poll_option_serializer"
|
|
require_relative "app/serializers/poll_serializer"
|
|
require_relative "jobs/regular/close_poll"
|
|
require_relative "lib/poll"
|
|
require_relative "lib/ranked_choice"
|
|
require_relative "lib/polls_updater"
|
|
require_relative "lib/polls_validator"
|
|
require_relative "lib/post_validator"
|
|
require_relative "lib/post_extension"
|
|
require_relative "lib/user_extension"
|
|
|
|
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.prepend(DiscoursePoll::PostExtension)
|
|
User.prepend(DiscoursePoll::UserExtension)
|
|
end
|
|
|
|
validate(:post, :validate_polls) do |force = nil|
|
|
return unless raw_changed? || force
|
|
|
|
validator = DiscoursePoll::PollsValidator.new(self)
|
|
return unless (polls = validator.validate_polls)
|
|
return if polls.blank? && id.blank?
|
|
|
|
if polls.present?
|
|
validator = DiscoursePoll::PostValidator.new(self)
|
|
return unless validator.validate_post
|
|
end
|
|
|
|
# are we updating a post?
|
|
if id.present?
|
|
return if polls.blank? && ::Poll.where(post: self).empty?
|
|
|
|
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|
|
|
html = +""
|
|
|
|
if title = poll.at_css(".poll-title")
|
|
html << title.to_html
|
|
end
|
|
|
|
if container = poll.at_css(".poll-container")
|
|
container.css("li").each { |li| li.remove_attribute("data-poll-option-id") }
|
|
html << container.inner_html
|
|
end
|
|
|
|
html << "<p><a href='#{post_url}'>#{I18n.t("poll.email.link_to_poll")}</a></p>"
|
|
|
|
poll.replace(html)
|
|
end
|
|
end
|
|
end
|
|
|
|
on(:reduce_excerpt) do |doc, options|
|
|
post = options[:post]
|
|
|
|
if post&.url.present?
|
|
replacement = "<a href='#{UrlHelper.normalized_encode(post.url)}'>#{I18n.t("poll.poll")}</a>"
|
|
else
|
|
replacement = 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(:post_moved) do |new_post, _original_topic_id, old_post|
|
|
next if old_post.blank? || new_post.id == old_post.id
|
|
|
|
ActiveRecord::Base.transaction do
|
|
# Remove empty polls auto-created by PostCreator on the new post
|
|
# so the originals (with votes) can be moved without a unique constraint violation.
|
|
new_polls = Poll.where(post_id: new_post.id)
|
|
if new_polls.exists?
|
|
PollVote.where(poll_id: new_polls.select(:id)).delete_all
|
|
PollOption.where(poll_id: new_polls.select(:id)).delete_all
|
|
new_polls.delete_all
|
|
end
|
|
|
|
Poll.where(post_id: old_post.id).update_all(post_id: new_post.id)
|
|
end
|
|
|
|
DiscoursePoll::PollsUpdater.update_post_custom_fields(new_post)
|
|
DiscoursePoll::PollsUpdater.update_post_custom_fields(old_post)
|
|
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 user_id = :source_user_id
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM poll_votes
|
|
WHERE user_id = :target_user_id
|
|
AND poll_votes.poll_id = poll_votes.poll_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?
|
|
all_polls = Poll.includes(:poll_options).where(post_id: post_with_polls)
|
|
Poll.preload!(all_polls, user_id: @user&.id)
|
|
DiscoursePoll::Poll.preload_serialized_voters!(all_polls)
|
|
all_polls.each do |p|
|
|
polls[p.post_id] ||= []
|
|
polls[p.post_id] << p
|
|
end
|
|
end
|
|
|
|
polls
|
|
end
|
|
end
|
|
|
|
add_to_serializer(:current_user, :can_create_poll) do
|
|
scope.user&.staff? || scope.user&.in_any_groups?(SiteSetting.poll_create_allowed_groups_map)
|
|
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: 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|
|
|
if poll.ranked_choice?
|
|
user_poll_votes =
|
|
poll
|
|
.poll_votes
|
|
.where(user_id: scope.user.id)
|
|
.joins(:poll_option)
|
|
.pluck("poll_options.digest", "poll_votes.rank")
|
|
.map { |digest, rank| { digest: digest, rank: rank } }
|
|
else
|
|
user_poll_votes =
|
|
poll
|
|
.poll_votes
|
|
.where(user_id: scope.user.id)
|
|
.joins(:poll_option)
|
|
.pluck("poll_options.digest")
|
|
end
|
|
[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
|
|
|
|
# Only single-choice (regular) polls are supported for anonymous voting.
|
|
# Multi-choice and ranked-choice polls require accumulating selections
|
|
# client-side, which is out of scope for the current anonymous flow.
|
|
register_anonymous_action("vote_poll") do |user, params|
|
|
options = params["options"]
|
|
next if !options.is_a?(Array) || options.size != 1
|
|
|
|
DiscoursePoll::Poll.vote(user, params["post_id"], params["poll_name"], options)
|
|
rescue DiscoursePoll::Error
|
|
# Expected business-logic failures (poll closed, group-restricted, etc.)
|
|
# — silently drop. Unexpected exceptions still bubble to AnonymousAction.consume.
|
|
end
|
|
end
|