# name: discourse-assign # about: Assign users to topics # version: 0.1 # authors: Sam Saffron register_asset 'stylesheets/assigns.scss' after_initialize do module ::DiscourseAssign class Engine < ::Rails::Engine engine_name "discourse_assign" isolate_namespace DiscourseAssign end end class ::TopicAssigner def self.backfill_auto_assign staff_mention = User.where('moderator OR admin') .pluck('username') .map{|name| "p.cooked ILIKE '%mention%@#{name}%'"} .join(' OR ') sql = < assign_to.username} }) unless @assigned_by.id == assign_to.id Notification.create!(notification_type: Notification.types[:custom], user_id: assign_to.id, topic_id: @topic.id, post_number: 1, data: { message: 'discourse_assign.assign_notification', display_username: @assigned_by.username, topic_title: @topic.title }.to_json ) end end true end def unassign(silent: false) if assigned_to_id = @topic.custom_fields["assigned_to_id"] @topic.custom_fields["assigned_to_id"] = nil @topic.custom_fields["assigned_by_id"] = nil @topic.save! post = @topic.posts.where(post_number: 1).first post.publish_change_to_clients!(:revised, { reload_topic: true }) assigned_user = User.find_by(id: assigned_to_id) UserAction.where( action_type: UserAction::ASSIGNED, target_post_id: post.id ).destroy_all # yank notification Notification.where( notification_type: Notification.types[:custom], user_id: assigned_user.try(:id), topic_id: @topic.id, post_number: 1 ).where("data like '%discourse_assign.assign_notification%'") .destroy_all if SiteSetting.unassign_creates_tracking_post && !silent post_type = SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper] @topic.add_moderator_post(@assigned_by, nil, { bump: false, post_type: post_type, custom_fields: {"action_code_who" => assigned_user&.username}, action_code: "unassigned"}) end end end end class ::DiscourseAssign::AssignController < Admin::AdminController before_filter :ensure_logged_in def suggestions users = [current_user] users += User .where('admin OR moderator') .where('users.id <> ?', current_user.id) .joins("join ( SELECT value::integer user_id, MAX(created_at) last_assigned FROM topic_custom_fields WHERE name = 'assigned_to_id' GROUP BY value::integer ) as X ON X.user_id = users.id") .order('X.last_assigned DESC') .limit(6) render json: ActiveModel::ArraySerializer.new(users, scope: guardian, each_serializer: BasicUserSerializer) end def unassign topic_id = params.require(:topic_id) topic = Topic.find(topic_id.to_i) assigner = TopicAssigner.new(topic, current_user) assigner.unassign render json: success_json end def assign topic_id = params.require(:topic_id) username = params.require(:username) topic = Topic.find(topic_id.to_i) assign_to = User.find_by(username_lower: username.downcase) raise Discourse::NotFound unless assign_to assigner = TopicAssigner.new(topic, current_user) # perhaps? #Scheduler::Defer.later "assign topic" do assigner.assign(assign_to) render json: success_json end class ::Topic def assigned_to_user @assigned_to_user || if user_id = custom_fields["assigned_to_id"] @assigned_to_user = User.find_by(id: user_id) end end def preload_assigned_to_user(assigned_to_user) @assigned_to_user = assigned_to_user end end TopicList.preloaded_custom_fields << "assigned_to_id" TopicList.on_preload do |topics, topic_list| is_staff = topic_list.current_user && topic_list.current_user.staff? allowed_access = SiteSetting.assigns_public || is_staff if allowed_access && topics.length > 0 users = User.where("id in ( SELECT value::int FROM topic_custom_fields WHERE name = 'assigned_to_id' AND topic_id IN (?) )", topics.map(&:id)) .select(:id, :email, :username, :uploaded_avatar_id) map = {} users.each{|u| map[u.id] = u} topics.each do |t| if id = t.custom_fields['assigned_to_id'] t.preload_assigned_to_user(map[id.to_i]) end end end end require_dependency 'topic_query' TopicQuery.add_custom_filter(:assigned) do |results, topic_query| if topic_query.guardian.is_staff? || SiteSetting.assigns_public username = topic_query.options[:assigned] user_id = topic_query.guardian.user.id if username == "me" special = ["*", "nobody"].include?(username) if username.present? && !special user_id ||= User.where(username_lower: username.downcase).pluck(:id).first end if user_id || special if username == "nobody" results = results.joins("LEFT JOIN topic_custom_fields tc_assign ON topics.id = tc_assign.topic_id AND tc_assign.name = 'assigned_to_id'") .where("tc_assign.name IS NULL") else if username == "*" filter = "AND tc_assign.value IS NOT NULL" else filter = "AND tc_assign.value = '#{user_id.to_i.to_s}'" end results = results.joins("JOIN topic_custom_fields tc_assign ON topics.id = tc_assign.topic_id AND tc_assign.name = 'assigned_to_id' #{filter} ") end end end results end require_dependency 'topic_list_item_serializer' class ::TopicListItemSerializer has_one :assigned_to_user, serializer: BasicUserSerializer, embed: :objects def include_assigned_to_user? (SiteSetting.assigns_public || scope.is_staff?) && object.assigned_to_user end end require_dependency 'topic_view_serializer' class ::TopicViewSerializer attributes :assigned_to_user def assigned_to_user if assigned_to_user_id && user = User.find_by(id: assigned_to_user_id) assigned_at = TopicCustomField.where( topic_id: object.topic.id, name: "assigned_to_id" ).pluck(:created_at).first { username: user.username, name: user.name, avatar_template: user.avatar_template, assigned_at: assigned_at } end end def include_assigned_to_user? if SiteSetting.assigns_public || scope.is_staff? # subtle but need to catch cases where stuff is not assigned object.topic.custom_fields.keys.include?("assigned_to_id") end end def assigned_to_user_id id = object.topic.custom_fields["assigned_to_id"] # a bit messy but race conditions can give us an array here, avoid id && id.to_i rescue nil end end require_dependency 'topic_query' class ::TopicQuery def list_private_messages_assigned(user) list = private_messages_for(user, :all) list = list.where("topics.id IN ( SELECT topic_id FROM topic_custom_fields WHERE name = 'assigned_to_id' AND value = ? )", user.id.to_s) create_list(:private_messages, {}, list) end end require_dependency 'list_controller' class ::ListController generate_message_route(:private_messages_assigned) end DiscourseAssign::Engine.routes.draw do put "/assign" => "assign#assign" put "/unassign" => "assign#unassign" get "/suggestions" => "assign#suggestions" end Discourse::Application.routes.append do mount ::DiscourseAssign::Engine, at: "/assign" get "topics/private-messages-assigned/:username" => "list#private_messages_assigned", as: "topics_private_messages_assigned", constraints: {username: /[\w.\-]+?/} end end on(:post_created) do |post| ::TopicAssigner.auto_assign(post, force: true) end on(:post_edited) do |post, topic_changed| ::TopicAssigner.auto_assign(post, force: true) end on(:move_to_inbox) do |info| if SiteSetting.unassign_on_group_archive && info[:group] if topic = info[:topic] if user_id = topic.custom_fields["prev_assigned_to_id"] if user = User.find_by(id: user_id.to_i) assigner = TopicAssigner.new(topic, Discourse.system_user) assigner.assign(user, silent: true) end end end end end on(:archive_message) do |info| if SiteSetting.unassign_on_group_archive && info[:group] topic = info[:topic] if user_id = topic.custom_fields["assigned_to_id"] if user = User.find_by(id: user_id.to_i) topic.custom_fields["prev_assigned_to_id"] = user.id topic.save assigner = TopicAssigner.new(topic, Discourse.system_user) assigner.unassign(silent: true) end end end end end