mirror of
https://github.com/discourse/discourse.git
synced 2026-08-11 02:59:07 +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>
252 lines
8.6 KiB
Ruby
Executable file
Vendored
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
|