0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/plugins/discourse-topic-voting/plugin.rb
Régis Hanol fba56920b5
FEATURE: Prompt anonymous users to sign up after engagement clicks (#40256)
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>
2026-05-26 09:30:38 +02:00

252 lines
8.6 KiB
Ruby
Executable file
Vendored

# frozen_string_literal: true
# name: discourse-topic-voting
# about: Adds the ability to vote on topics in a specified category.
# meta_topic_id: 40121
# version: 0.5
# author: Joe Buhlig joebuhlig.com, Sam Saffron
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-topic-voting
register_asset "stylesheets/common/topic-voting.scss"
register_svg_icon "check-to-slot"
register_svg_icon "vote-up"
register_svg_icon "vote-up-filled"
enabled_site_setting :topic_voting_enabled
Discourse.top_menu_items.push(:votes)
Discourse.anonymous_top_menu_items.push(:votes)
Discourse.filters.push(:votes)
Discourse.anonymous_filters.push(:votes)
module ::DiscourseTopicVoting
PLUGIN_NAME = "discourse-topic-voting"
ENABLE_TOPIC_VOTING_SETTING = "enable_topic_voting"
VOTER_PREVIEW_LIMIT = 104
BADGE_NAMES = %w[Daydreamer Brainstormer Innovator Visionary].freeze
end
require_relative "lib/discourse_topic_voting/badge_queries"
require_relative "lib/discourse_topic_voting/engine"
require_relative "lib/discourse_topic_voting/topic_votes_filter"
after_initialize do
SeedFu.fixture_paths << Rails.root.join("plugins/discourse-topic-voting/db/fixtures").to_s
reloadable_patch do
register_category_type(DiscourseTopicVoting::Categories::Types::Ideas)
CategoriesController.prepend(DiscourseTopicVoting::CategoriesControllerExtension)
Category.prepend(DiscourseTopicVoting::CategoryExtension)
ListController.prepend(DiscourseTopicVoting::ListControllerExtension)
Topic.prepend(DiscourseTopicVoting::TopicExtension)
TopicQuery.prepend(DiscourseTopicVoting::TopicQueryExtension)
User.prepend(DiscourseTopicVoting::UserExtension)
WebHook.prepend(DiscourseTopicVoting::WebHookExtension)
end
add_to_serializer(:post, :can_vote, include_condition: -> { object.post_number == 1 }) do
object.topic&.can_vote?
end
add_to_serializer(:topic_view, :can_vote) { object.topic.can_vote? }
add_to_serializer(:topic_view, :vote_count) { object.topic.vote_count }
add_to_serializer(:topic_view, :user_voted) do
scope.user ? object.topic.user_voted?(scope.user) : false
end
TopicQuery.results_filter_callbacks << ->(_type, result, user, options) do
return result unless SiteSetting.topic_voting_enabled
result = result.preload(:topic_vote_count)
if user
result =
result.select(
"topics.*, COALESCE((SELECT 1 FROM topic_voting_votes WHERE user_id = #{user.id} AND topic_id = topics.id), 0) AS current_user_voted",
)
if options[:state] == "my_votes"
result =
result.joins(
"INNER JOIN topic_voting_votes ON topic_voting_votes.topic_id = topics.id AND topic_voting_votes.user_id = #{user.id} AND topic_voting_votes.archive = FALSE",
)
end
end
if options[:order] == "votes"
sort_dir = (options[:ascending] == "true") ? "ASC" : "DESC"
result =
result.joins(
"LEFT JOIN topic_voting_topic_vote_count ON topic_voting_topic_vote_count.topic_id = topics.id",
).reorder(
"COALESCE(topic_voting_topic_vote_count.votes_count,'0')::integer #{sort_dir}, topics.bumped_at DESC",
)
end
result
end
register_category_custom_field_type(DiscourseTopicVoting::ENABLE_TOPIC_VOTING_SETTING, :boolean)
register_preloaded_category_custom_fields DiscourseTopicVoting::ENABLE_TOPIC_VOTING_SETTING
add_to_serializer(:topic_list_item, :vote_count, include_condition: -> { object.can_vote? }) do
object.vote_count
end
add_to_serializer(:topic_list_item, :can_vote, include_condition: -> { object.regular? }) do
object.can_vote?
end
add_to_serializer(:topic_list_item, :user_voted, include_condition: -> { object.can_vote? }) do
object.user_voted?(scope.user) if scope.user
end
add_to_serializer(
:basic_category,
:can_vote,
include_condition: -> { Category.can_vote?(object.id) },
) { true }
register_search_advanced_filter(/^min_vote_count:(\d+)$/) do |posts, match|
posts.where(
"(SELECT votes_count FROM topic_voting_topic_vote_count WHERE topic_voting_topic_vote_count.topic_id = posts.topic_id) >= ?",
match.to_i,
)
end
register_search_advanced_order(:votes) do |posts|
posts.reorder(
"COALESCE((SELECT dvtvc.votes_count FROM topic_voting_topic_vote_count dvtvc WHERE dvtvc.topic_id = topics.id), 0) DESC",
)
end
register_modifier(:badge_granter_suppress_notification) do |suppress, badge, granted_at, _|
next true if DiscourseTopicVoting::BADGE_NAMES.include?(badge.name) && granted_at < 2.weeks.ago
suppress
end
register_modifier(:topics_filter_options) do |results, _guardian|
results.concat(
[
{
name: "votes-min:",
description: I18n.t("topic_voting.filter.description.topic_votes_min"),
type: "number",
},
{
name: "votes-max:",
description: I18n.t("topic_voting.filter.description.topic_votes_max"),
type: "number",
},
{
name: "order:votes",
description: I18n.t("topic_voting.filter.description.order_topic_votes"),
},
{
name: "order:votes-asc",
description: I18n.t("topic_voting.filter.description.order_topic_votes_asc"),
},
],
)
results
end
add_to_serializer(:current_user, :votes_exceeded) { object.reached_voting_limit? }
add_to_serializer(:current_user, :votes_count) { object.vote_count }
add_to_serializer(:current_user, :votes_left) { object.votes_left }
add_to_serializer(:current_user, :vote_limit) { object.vote_limit }
topic_votes_value_from = ->(values) do
value = values&.last
return if value.blank? || value !~ /\A\d+\z/
value.to_i
end
add_filter_custom_filter("votes-min") do |scope, filter_values, _guardian|
value = topic_votes_value_from.call(filter_values)
next scope if value.nil?
DiscourseTopicVoting::TopicVotesFilter.apply(scope, min_votes: value)
end
add_filter_custom_filter("votes-max") do |scope, filter_values, _guardian|
value = topic_votes_value_from.call(filter_values)
next scope if value.nil?
DiscourseTopicVoting::TopicVotesFilter.apply(scope, max_votes: value)
end
add_filter_custom_filter("order:votes") do |scope, order_direction, _guardian|
DiscourseTopicVoting::TopicVotesFilter.apply(scope, order_direction:)
end
on(:topic_status_updated) do |topic, status, enabled|
next if topic.trashed?
next if %w[closed autoclosed archived].exclude?(status)
if enabled
Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteRelease, topic_id: topic.id)
else
is_closing_unarchived = %w[closed autoclosed].include?(status) && !topic.archived
is_archiving_open = status == "archived" && !topic.closed
if is_closing_unarchived || is_archiving_open
Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteReclaim, topic_id: topic.id)
end
end
end
on(:topic_trashed) do |topic|
if !topic.closed && !topic.archived
Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteRelease, topic_id: topic.id, trashed: true)
end
end
on(:topic_recovered) do |topic|
if !topic.closed && !topic.archived
Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteReclaim, topic_id: topic.id)
end
end
on(:post_edited) do |post, _, revisor|
if SiteSetting.topic_voting_enabled && revisor.topic_diff.has_key?("category_id") &&
DiscourseTopicVoting::Vote.exists?(topic_id: post.topic_id) && !post.topic.closed &&
!post.topic.archived && !post.topic.trashed?
new_category_id = post.reload.topic.category_id
if Category.can_vote?(new_category_id)
Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteReclaim, topic_id: post.topic_id)
else
Jobs.enqueue(Jobs::DiscourseTopicVoting::VoteRelease, topic_id: post.topic_id)
end
end
end
on(:topic_merged) { |orig, dest| DiscourseTopicVoting::TopicMerger.merge(orig, dest) }
on(:merging_users) do |source_user, target_user|
DiscourseTopicVoting::UserMerger.merge(source_user, target_user)
end
Discourse::Application.routes.prepend do
get "c/*category_slug_path_with_id/l/votes.rss" => "list#votes_feed", :format => :rss
end
Discourse::Application.routes.append do
mount DiscourseTopicVoting::Engine, at: "/voting"
get "topics/voted-by/:username" => "list#voted_by",
:as => "voted_by",
:constraints => {
username: RouteFormat.username,
}
end
register_anonymous_action("vote_topic") do |user, params|
DiscourseTopicVoting::Votes::Cast.call(
params: {
topic_id: params["topic_id"],
},
guardian: user.guardian,
)
end
end