discourse-topic-voting/plugin.rb

314 lines
12 KiB
Ruby
Executable File

# frozen_string_literal: true
# name: discourse-topic-voting
# about: Adds the ability to vote on features in a specified category.
# version: 0.5
# author: Joe Buhlig joebuhlig.com, Sam Saffron
# url: https://github.com/discourse/discourse-topic-voting
register_asset "stylesheets/common/topic-voting.scss"
register_asset "stylesheets/desktop/topic-voting.scss", :desktop
register_asset "stylesheets/mobile/topic-voting.scss", :mobile
enabled_site_setting :voting_enabled
Discourse.top_menu_items.push(:votes)
Discourse.anonymous_top_menu_items.push(:votes)
Discourse.filters.push(:votes)
Discourse.anonymous_filters.push(:votes)
after_initialize do
module ::DiscourseTopicVoting
class Engine < ::Rails::Engine
isolate_namespace DiscourseTopicVoting
end
end
load File.expand_path('../app/jobs/onceoff/voting_ensure_consistency.rb', __FILE__)
load File.expand_path('../app/models/discourse_topic_voting/category_setting.rb', __FILE__)
load File.expand_path('../app/models/discourse_topic_voting/topic_vote_count.rb', __FILE__)
load File.expand_path('../app/models/discourse_topic_voting/vote.rb', __FILE__)
load File.expand_path('../lib/discourse_topic_voting/categories_controller_extension.rb', __FILE__)
load File.expand_path('../lib/discourse_topic_voting/category_extension.rb', __FILE__)
load File.expand_path('../lib/discourse_topic_voting/topic_extension.rb', __FILE__)
load File.expand_path('../lib/discourse_topic_voting/user_extension.rb', __FILE__)
reloadable_patch do
CategoriesController.class_eval { prepend DiscourseTopicVoting::CategoriesControllerExtension }
Category.class_eval { prepend DiscourseTopicVoting::CategoryExtension }
Topic.class_eval { prepend DiscourseTopicVoting::TopicExtension }
User.class_eval { prepend DiscourseTopicVoting::UserExtension }
add_to_serializer(:post, :can_topic_vote, false) { object.topic&.can_topic_vote? }
add_to_serializer(:post, :include_can_topic_vote?) { SiteSetting.voting_enabled && object.post_number == 1 }
add_to_serializer(:topic_view, :can_topic_vote) { object.topic.can_topic_vote? }
add_to_serializer(:topic_view, :topic_vote_count) { object.topic.topic_topic_vote_count }
add_to_serializer(:topic_view, :user_topic_voted) { scope.user ? object.topic.user_topic_voted?(scope.user) : false }
if TopicQuery.respond_to?(:results_filter_callbacks)
TopicQuery.results_filter_callbacks << ->(_type, result, user, options) {
result = result.includes(:topic_vote_count)
if user
result = result.select("topics.*, COALESCE((SELECT 1 FROM discourse_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 discourse_voting_votes ON discourse_voting_votes.topic_id = topics.id AND discourse_voting_votes.user_id = #{user.id}")
end
end
if options[:order] == "votes"
sort_dir = (options[:ascending] == "true") ? "ASC" : "DESC"
result = result
.joins("LEFT JOIN discourse_voting_topic_vote_count ON discourse_voting_topic_vote_count.topic_id = topics.id")
.reorder("COALESCE(discourse_voting_topic_vote_count.votes_count,'0')::integer #{sort_dir}, topics.bumped_at DESC")
end
result
}
end
add_to_serializer(:category, :custom_fields) do
object.custom_fields.merge(enable_topic_voting: DiscourseTopicVoting::CategorySetting.find_by(category_id: object.id).present?)
end
add_to_serializer(:topic_list_item, :topic_vote_count, false) { object.topic_topic_vote_count }
add_to_serializer(:topic_list_item, :can_topic_vote, false) { object.can_topic_vote? }
add_to_serializer(:topic_list_item, :user_topic_voted, false) { object.user_topic_voted?(scope.user) if scope.user }
add_to_serializer(:topic_list_item, :include_topic_vote_count?) { object.can_topic_vote? }
add_to_serializer(:topic_list_item, :include_can_topic_vote?) { SiteSetting.voting_enabled && object.regular? }
add_to_serializer(:topic_list_item, :include_user_topic_voted?) { object.can_topic_vote? }
add_to_serializer(:basic_category, :can_vote, false) { true }
add_to_serializer(:basic_category, :include_can_vote?) { Category.can_vote?(object.id) }
register_search_advanced_filter(/^min_vote_count:(\d+)$/) do |posts, match|
posts.where("(SELECT votes_count FROM discourse_voting_topic_vote_count WHERE discourse_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 discourse_voting_topic_vote_count dvtvc WHERE dvtvc.topic_id = topics.id), 0) DESC")
end
class ::Category
def self.reset_voting_cache
@allowed_voting_cache["allowed"] =
begin
DiscourseTopicVoting::CategorySetting.pluck(:category_id)
end
end
@allowed_voting_cache = DistributedCache.new("allowed_voting")
def self.can_vote?(category_id)
return false unless SiteSetting.voting_enabled
unless set = @allowed_voting_cache["allowed"]
set = reset_voting_cache
end
set.include?(category_id)
end
after_save :reset_voting_cache
protected
def reset_voting_cache
::Category.reset_voting_cache
end
end
require_dependency 'user'
class ::User
def topic_vote_count
topics_with_topic_vote.length
end
def alert_low_topic_votes?
(topic_vote_limit - topic_vote_count) <= SiteSetting.voting_alert_votes_left
end
def topics_with_topic_vote
self.votes.where(archive: false)
end
def topics_with_archived_topic_vote
self.votes.where(archive: true)
end
def reached_topic_voting_limit?
topic_vote_count >= topic_vote_limit
end
def topic_vote_limit
SiteSetting.public_send("voting_tl#{self.trust_level}_vote_limit")
end
end
add_to_serializer(:current_user, :topic_votes_exceeded) { object.reached_topic_voting_limit? }
add_to_serializer(:current_user, :topic_votes_count) { object.topic_vote_count }
add_to_serializer(:current_user, :topic_votes_left) { [object.topic_vote_limit - object.topic_vote_count, 0].max }
require_dependency 'list_controller'
class ::ListController
skip_before_action :ensure_logged_in, only: %i[voted_by]
def voted_by
if SiteSetting.voting_show_votes_on_profile
list_opts = build_topic_list_options
target_user = fetch_user_from_params(include_inactive: current_user.try(:staff?))
list = generate_list_for("voted_by", target_user, list_opts)
list.more_topics_url = url_for(construct_url_with(:next, list_opts))
list.prev_topics_url = url_for(construct_url_with(:prev, list_opts))
respond_with_list(list)
else
respond_to do |format|
format.html { render nothing: true, status: 404 }
format.json { render json: failed_json, status: 404 }
end
end
end
end
require_dependency 'topic_query'
class ::TopicQuery
def list_voted_by(user)
create_list(:user_topics) do |topics|
topics
.joins("INNER JOIN discourse_voting_votes ON discourse_voting_votes.topic_id = topics.id")
.where("discourse_voting_votes.user_id = ?", user.id)
end
end
def list_votes
create_list(:votes, unordered: true) do |topics|
topics.joins("LEFT JOIN discourse_voting_topic_vote_count dvtvc ON dvtvc.topic_id = topics.id")
.order("COALESCE(dvtvc.votes_count,'0')::integer DESC, topics.bumped_at DESC")
end
end
end
require_dependency "jobs/base"
module ::Jobs
class VoteRelease < ::Jobs::Base
def execute(args)
if topic = Topic.with_deleted.find_by(id: args[:topic_id])
votes = DiscourseTopicVoting::Vote.where(topic_id: args[:topic_id])
votes.update_all(archive: true)
topic.update_vote_count
return if args[:trashed]
votes.find_each do |vote|
Notification.create!(user_id: vote.user_id,
notification_type: Notification.types[:votes_released],
topic_id: vote.topic_id,
data: { message: "votes_released",
title: "votes_released" }.to_json)
rescue
# If one notifcation crash, inform others
end
end
end
end
class VoteReclaim < ::Jobs::Base
def execute(args)
if topic = Topic.with_deleted.find_by(id: args[:topic_id])
ActiveRecord::Base.transaction do
DiscourseTopicVoting::Vote.where(topic_id: args[:topic_id]).update_all(archive: false)
topic.update_vote_count
end
end
end
end
end
end
DiscourseEvent.on(:topic_status_updated) do |topic, status, enabled|
if (status == 'closed' || status == 'autoclosed' || status == 'archived') && enabled == true
Jobs.enqueue(:vote_release, topic_id: topic.id)
end
if (status == 'closed' || status == 'autoclosed' || status == 'archived') && enabled == false
Jobs.enqueue(:vote_reclaim, topic_id: topic.id)
end
end
DiscourseEvent.on(:topic_trashed) do |topic|
Jobs.enqueue(:vote_release, topic_id: topic.id, trashed: true) if !topic.closed && !topic.archived
end
DiscourseEvent.on(:topic_recovered) do |topic|
Jobs.enqueue(:vote_reclaim, topic_id: topic.id) if !topic.closed && !topic.archived
end
DiscourseEvent.on(:post_edited) do |post, topic_changed|
if topic_changed &&
SiteSetting.voting_enabled &&
DiscourseTopicVoting::Vote.exists?(topic_id: post.topic_id)
new_category_id = post.reload.topic.category_id
if Category.can_vote?(new_category_id)
Jobs.enqueue(:vote_reclaim, topic_id: post.topic_id)
else
Jobs.enqueue(:vote_release, topic_id: post.topic_id)
end
end
end
DiscourseEvent.on(:topic_merged) do |orig, dest|
moved_votes = 0
duplicated_votes = 0
if orig.who_voted.present? && orig.closed
orig.who_voted.each do |user|
next if user.blank?
user_votes = user.topics_with_topic_vote.pluck(:topic_id)
user_archived_votes = user.topics_with_archived_topic_vote.pluck(:topic_id)
if user_votes.include?(orig.id) || user_archived_votes.include?(orig.id)
if user_votes.include?(dest.id) || user_archived_votes.include?(dest.id)
duplicated_votes += 1
user.votes.destroy_by(topic_id: orig.id)
else
user.votes.find_by(topic_id: orig.id, user_id: user.id).update!(topic_id: dest.id, archive: dest.closed)
moved_votes += 1
end
else
next
end
end
end
if moved_votes > 0
orig.update_vote_count
dest.update_vote_count
if moderator_post = orig.ordered_posts.where(action_code: 'split_topic').last
moderator_post.raw << "\n\n#{I18n.t('topic_voting.votes_moved', count: moved_votes)}"
moderator_post.raw << " #{I18n.t('topic_voting.duplicated_votes', count: duplicated_votes)}" if duplicated_votes > 0
moderator_post.save!
end
end
end
require File.expand_path(File.dirname(__FILE__) + '/app/controllers/discourse_topic_voting/votes_controller')
DiscourseTopicVoting::Engine.routes.draw do
post 'vote' => 'votes#vote'
post 'unvote' => 'votes#unvote'
get 'who' => 'votes#who'
end
Discourse::Application.routes.append do
mount ::DiscourseTopicVoting::Engine, at: "/voting"
# USERNAME_ROUTE_FORMAT is deprecated but we may need to support it for older installs
username_route_format = defined?(RouteFormat) ? RouteFormat.username : USERNAME_ROUTE_FORMAT
get "topics/voted-by/:username" => "list#voted_by", as: "voted_by", constraints: { username: username_route_format }
end
end