# frozen_string_literal: true # name: discourse-assign # about: Assign users to topics # version: 1.0.1 # authors: Sam Saffron # url: https://github.com/discourse/discourse-assign # transpile_js: true enabled_site_setting :assign_enabled register_asset "stylesheets/assigns.scss" register_asset "stylesheets/mobile/assigns.scss", :mobile %w[user-plus user-times group-plus group-times].each { |i| register_svg_icon(i) } load File.expand_path("../lib/discourse_assign/engine.rb", __FILE__) load File.expand_path("../lib/discourse_assign/helpers.rb", __FILE__) load File.expand_path("../lib/validators/assign_statuses_validator.rb", __FILE__) 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: ::RouteFormat.username, } get "/topics/messages-assigned/:username" => "list#messages_assigned", :constraints => { username: ::RouteFormat.username, }, :as => "messages_assigned" get "/topics/group-topics-assigned/:groupname" => "list#group_topics_assigned", :constraints => { username: ::RouteFormat.username, }, :as => "group_topics_assigned" get "/g/:id/assigned" => "groups#index" get "/g/:id/assigned/:route_type" => "groups#index" end after_initialize do require File.expand_path("../jobs/scheduled/enqueue_reminders.rb", __FILE__) require File.expand_path("../jobs/regular/remind_user.rb", __FILE__) require File.expand_path("../jobs/regular/assign_notification.rb", __FILE__) require File.expand_path("../jobs/regular/unassign_notification.rb", __FILE__) require "topic_assigner" require "assigner" require "pending_assigns_reminder" register_group_param(:assignable_level) register_groups_callback_for_users_search_controller_action(:assignable_groups) do |groups, user| groups.assignable(user) end reloadable_patch do |plugin| class ::Topic has_one :assignment, as: :target, dependent: :destroy end class ::Post has_one :assignment, as: :target, dependent: :destroy end class ::Group scope :assignable, ->(user) { where( "assignable_level in (:levels) OR ( assignable_level = #{ALIAS_LEVELS[:members_mods_and_admins]} AND id in ( SELECT group_id FROM group_users WHERE user_id = :user_id) ) OR ( assignable_level = #{ALIAS_LEVELS[:owners_mods_and_admins]} AND id in ( SELECT group_id FROM group_users WHERE user_id = :user_id AND owner IS TRUE) )", levels: alias_levels(user), user_id: user && user.id, ) } end end frequency_field = PendingAssignsReminder::REMINDERS_FREQUENCY register_editable_user_custom_field frequency_field User.register_custom_field_type frequency_field, :integer DiscoursePluginRegistry.serialized_current_user_fields << frequency_field add_to_serializer(:user, :reminders_frequency) { RemindAssignsFrequencySiteSettings.values } add_to_serializer(:group_show, :assignment_count) do Topic.joins(<<~SQL).where(<<~SQL, group_id: object.id).where("topics.deleted_at IS NULL").count JOIN assignments a ON topics.id = a.topic_id AND a.assigned_to_id IS NOT NULL SQL a.active AND (( a.assigned_to_type = 'User' AND a.assigned_to_id IN ( SELECT group_users.user_id FROM group_users WHERE group_id = :group_id ) ) OR ( a.assigned_to_type = 'Group' AND a.assigned_to_id = :group_id )) SQL end add_to_serializer(:group_show, "include_assignment_count?") { scope.can_assign? } add_to_serializer(:group_show, :assignable_level) { object.assignable_level } add_to_serializer(:group_show, :can_show_assigned_tab?) { object.can_show_assigned_tab? } add_model_callback(UserCustomField, :before_save) do self.value = self.value.to_i if self.name == frequency_field end add_class_method(:group, :assign_allowed_groups) do allowed_groups = SiteSetting.assign_allowed_on_groups.split("|") where(id: allowed_groups) end add_to_class(:user, :can_assign?) do @can_assign ||= begin return true if admin? allowed_groups = SiteSetting.assign_allowed_on_groups.split("|").compact allowed_groups.present? && groups.where(id: allowed_groups).exists? ? :true : :false end @can_assign == :true end add_to_serializer(:current_user, :never_auto_track_topics) do ( user.user_option.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs ) < 0 end add_to_class(:group, :can_show_assigned_tab?) do allowed_group_ids = SiteSetting.assign_allowed_on_groups.split("|") group_has_disallowed_users = DB.query_single(<<~SQL, allowed_group_ids: allowed_group_ids, current_group_id: self.id)[0] SELECT EXISTS( SELECT 1 FROM users JOIN group_users current_group_users ON current_group_users.user_id=users.id AND current_group_users.group_id = :current_group_id LEFT JOIN group_users allowed_group_users ON allowed_group_users.user_id=users.id AND allowed_group_users.group_id IN (:allowed_group_ids) WHERE allowed_group_users.user_id IS NULL ) SQL !group_has_disallowed_users end add_to_class(:guardian, :can_assign?) { user && user.can_assign? } add_class_method(:user, :assign_allowed) do allowed_groups = SiteSetting.assign_allowed_on_groups.split("|") # The UNION against admin users is necessary because bot users like the system user are given the admin status but # are not added into the admin group. where( "users.id IN ( SELECT user_id FROM group_users WHERE group_users.group_id IN (?) UNION SELECT id FROM users WHERE users.admin )", allowed_groups, ) end add_model_callback(Group, :before_update) do if name_changed? SiteSetting.assign_allowed_on_groups = SiteSetting.assign_allowed_on_groups.gsub(name_was, name) end end add_model_callback(Group, :before_destroy) do new_setting = SiteSetting.assign_allowed_on_groups.gsub(/#{id}[|]?/, "") new_setting = new_setting.chomp("|") if new_setting.ends_with?("|") SiteSetting.assign_allowed_on_groups = new_setting end on(:assign_topic) do |topic, user, assigning_user, force| Assigner.new(topic, assigning_user).assign(user) if force || !Assignment.exists?(target: topic) end on(:unassign_topic) { |topic, unassigning_user| Assigner.new(topic, unassigning_user).unassign } Site.preloaded_category_custom_fields << "enable_unassigned_filter" BookmarkQuery.on_preload do |bookmarks, bookmark_query| if SiteSetting.assign_enabled? topics = Bookmark .select_type(bookmarks, "Topic") .map(&:bookmarkable) .concat(Bookmark.select_type(bookmarks, "Post").map { |bm| bm.bookmarkable.topic }) .uniq assignments = Assignment .strict_loading .where(topic_id: topics) .includes(:assigned_to) .index_by(&:topic_id) topics.each do |topic| assigned_to = assignments[topic.id]&.assigned_to topic.preload_assigned_to(assigned_to) end end end TopicView.on_preload do |topic_view| topic_view.instance_variable_set(:@posts, topic_view.posts.includes(:assignment)) end TopicList.on_preload do |topics, topic_list| if SiteSetting.assign_enabled? can_assign = topic_list.current_user && topic_list.current_user.can_assign? allowed_access = SiteSetting.assigns_public || can_assign if allowed_access && topics.length > 0 assignments = Assignment.strict_loading.where(topic: topics, active: true).includes(:target) assignments_map = assignments.group_by(&:topic_id) user_ids = assignments.filter { |assignment| assignment.assigned_to_user? }.map(&:assigned_to_id) users_map = User.where(id: user_ids).select(UserLookup.lookup_columns).index_by(&:id) group_ids = assignments.filter { |assignment| assignment.assigned_to_group? }.map(&:assigned_to_id) groups_map = Group.where(id: group_ids).index_by(&:id) topics.each do |topic| assignments = assignments_map[topic.id] direct_assignment = assignments&.find do |assignment| assignment.target_type == "Topic" && assignment.target_id == topic.id end indirectly_assigned_to = {} assignments &.each do |assignment| next if assignment.target_type == "Topic" next if !assignment.target next( indirectly_assigned_to[assignment.target_id] = { assigned_to: users_map[assignment.assigned_to_id], post_number: assignment.target.post_number, } ) if assignment&.assigned_to_user? next( indirectly_assigned_to[assignment.target_id] = { assigned_to: groups_map[assignment.assigned_to_id], post_number: assignment.target.post_number, } ) if assignment&.assigned_to_group? end &.compact &.uniq assigned_to = if direct_assignment&.assigned_to_user? users_map[direct_assignment.assigned_to_id] elsif direct_assignment&.assigned_to_group? groups_map[direct_assignment.assigned_to_id] end topic.preload_assigned_to(assigned_to) topic.preload_indirectly_assigned_to(indirectly_assigned_to) end end end end Search.on_preload do |results, search| if SiteSetting.assign_enabled? can_assign = search.guardian&.can_assign? allowed_access = SiteSetting.assigns_public || can_assign if allowed_access && results.posts.length > 0 topics = results.posts.map(&:topic) assignments = Assignment .strict_loading .where(topic: topics, active: true) .includes(:assigned_to, :target) .group_by(&:topic_id) results.posts.each do |post| topic_assignments = assignments[post.topic.id] direct_assignment = topic_assignments&.find { |assignment| assignment.target_type == "Topic" } indirect_assignments = topic_assignments&.select { |assignment| assignment.target_type == "Post" } if direct_assignment assigned_to = direct_assignment.assigned_to post.topic.preload_assigned_to(assigned_to) end if indirect_assignments.present? indirect_assignment_map = indirect_assignments.reduce({}) do |acc, assignment| if assignment.target acc[assignment.target_id] = { assigned_to: assignment.assigned_to, post_number: assignment.target.post_number, } end acc end post.topic.preload_indirectly_assigned_to(indirect_assignment_map) end end end end end # TopicQuery require_dependency "topic_query" TopicQuery.add_custom_filter(:assigned) do |results, topic_query| name = topic_query.options[:assigned] next results if name.blank? next results if !topic_query.guardian.can_assign? && !SiteSetting.assigns_public if name == "nobody" next( results.joins("LEFT JOIN assignments a ON a.topic_id = topics.id AND active").where( "a.assigned_to_id IS NULL", ) ) end if name == "*" next( results.joins("JOIN assignments a ON a.topic_id = topics.id AND active").where( "a.assigned_to_id IS NOT NULL", ) ) end user_id = topic_query.guardian.user.id if name == "me" user_id ||= User.where(username_lower: name.downcase).pluck_first(:id) if user_id next( results.joins("JOIN assignments a ON a.topic_id = topics.id AND active").where( "a.assigned_to_id = ? AND a.assigned_to_type = 'User'", user_id, ) ) end group_id = Group.where(name: name.downcase).pluck_first(:id) if group_id next( results.joins("JOIN assignments a ON a.topic_id = topics.id AND active").where( "a.assigned_to_id = ? AND a.assigned_to_type = 'Group'", group_id, ) ) end next results end add_to_class(:topic_query, :list_messages_assigned) do |user, ignored_assignment_ids: nil| list = default_results(include_pms: true) where_clause = +"(" where_clause << "(assigned_to_id = :user_id AND assigned_to_type = 'User' AND active)" if @options[:filter] != :direct where_clause << "OR (assigned_to_id IN (group_users.group_id) AND assigned_to_type = 'Group' AND active)" end where_clause << ")" if ignored_assignment_ids.present? where_clause << "AND assignments.id NOT IN (:ignored_assignment_ids)" end topic_ids_sql = +<<~SQL SELECT topic_id FROM assignments LEFT JOIN group_users ON group_users.user_id = :user_id WHERE #{where_clause} SQL where_args = { user_id: user.id } where_args[:ignored_assignment_ids] = ignored_assignment_ids if ignored_assignment_ids.present? list = list.where("topics.id IN (#{topic_ids_sql})", **where_args).includes(:allowed_users) create_list(:assigned, { unordered: true }, list) end add_to_class(:topic_query, :group_topics_assigned_results) do |group| list = default_results(include_all_pms: true) topic_ids_sql = +<<~SQL SELECT topic_id FROM assignments WHERE ( assigned_to_id = :group_id AND assigned_to_type = 'Group' AND active ) SQL topic_ids_sql << <<~SQL if @options[:filter] != :direct OR ( assigned_to_id IN (SELECT user_id from group_users where group_id = :group_id) AND assigned_to_type = 'User' AND active ) SQL sql = "topics.id IN (#{topic_ids_sql})" list = list.where(sql, group_id: group.id).includes(:allowed_users) end add_to_class(:topic_query, :list_group_topics_assigned) do |group| create_list(:assigned, { unordered: true }, group_topics_assigned_results(group)) end add_to_class(:topic_query, :list_private_messages_assigned) do |user| list = private_messages_assigned_query(user) create_list(:private_messages, {}, list) end add_to_class(:topic_query, :private_messages_assigned_query) do |user| list = private_messages_for(user, :all) group_ids = user.groups.map(&:id) list = list.where(<<~SQL, user_id: user.id, group_ids: group_ids) topics.id IN ( SELECT topic_id FROM assignments WHERE active AND ((assigned_to_id = :user_id AND assigned_to_type = 'User') OR (assigned_to_id IN (:group_ids) AND assigned_to_type = 'Group')) ) SQL end # ListController require_dependency "list_controller" class ::ListController generate_message_route(:private_messages_assigned) end add_to_class(:list_controller, :messages_assigned) do user = User.find_by_username(params[:username]) raise Discourse::NotFound unless user raise Discourse::InvalidAccess unless current_user.can_assign? list_opts = build_topic_list_options list_opts.merge!({ filter: :direct }) if params[:direct] == "true" list = generate_list_for("messages_assigned", user, list_opts) list.more_topics_url = construct_url_with(:next, list_opts) list.prev_topics_url = construct_url_with(:prev, list_opts) respond_with_list(list) end add_to_class(:list_controller, :group_topics_assigned) do group = Group.find_by("name = ?", params[:groupname]) guardian.ensure_can_see_group_members!(group) raise Discourse::NotFound unless group raise Discourse::InvalidAccess unless current_user.can_assign? raise Discourse::InvalidAccess unless group.can_show_assigned_tab? list_opts = build_topic_list_options list_opts.merge!({ filter: :direct }) if params[:direct] == "true" list = generate_list_for("group_topics_assigned", group, list_opts) list.more_topics_url = construct_url_with(:next, list_opts) list.prev_topics_url = construct_url_with(:prev, list_opts) respond_with_list(list) end # Topic add_to_class(:topic, :assigned_to) do return @assigned_to if defined?(@assigned_to) @assigned_to = assignment.assigned_to if assignment&.active end add_to_class(:topic, :indirectly_assigned_to) do return @indirectly_assigned_to if defined?(@indirectly_assigned_to) @indirectly_assigned_to = Assignment .where(topic_id: id, target_type: "Post", active: true) .includes(:target) .inject({}) do |acc, assignment| if assignment.target acc[assignment.target_id] = { assigned_to: assignment.assigned_to, post_number: assignment.target.post_number, assignment_note: assignment.note, } acc[assignment.target_id][ :assignment_status ] = assignment.status if SiteSetting.enable_assign_status end acc end end add_to_class(:topic, :preload_assigned_to) { |assigned_to| @assigned_to = assigned_to } add_to_class(:topic, :preload_indirectly_assigned_to) do |indirectly_assigned_to| @indirectly_assigned_to = indirectly_assigned_to end # TopicList serializer add_to_serializer(:topic_list, :assigned_messages_count) do TopicQuery .new(object.current_user, guardian: scope, limit: false) .private_messages_assigned_query(object.current_user) .count end add_to_serializer(:topic_list, "include_assigned_messages_count?") do options = object.instance_variable_get(:@opts) if assigned_user = options.dig(:assigned) scope.can_assign? || assigned_user.downcase == scope.current_user&.username_lower end end # TopicView serializer add_to_serializer(:topic_view, :assigned_to_user, false) do DiscourseAssign::Helpers.build_assigned_to_user(object.topic.assigned_to, object.topic) end add_to_serializer(:topic_view, :include_assigned_to_user?) do (SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to&.is_a?(User) end add_to_serializer(:topic_view, :assigned_to_group, false) do DiscourseAssign::Helpers.build_assigned_to_group(object.topic.assigned_to, object.topic) end add_to_serializer(:topic_view, :include_assigned_to_group?) do (SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to&.is_a?(Group) end add_to_serializer(:topic_view, :indirectly_assigned_to) do DiscourseAssign::Helpers.build_indirectly_assigned_to( object.topic.indirectly_assigned_to, object.topic, ) end add_to_serializer(:topic_view, :include_indirectly_assigned_to?) do (SiteSetting.assigns_public || scope.can_assign?) && object.topic.indirectly_assigned_to.present? end add_to_serializer(:topic_view, :assignment_note, false) { object.topic.assignment.note } add_to_serializer(:topic_view, :include_assignment_note?, false) do (SiteSetting.assigns_public || scope.can_assign?) && object.topic.assignment.present? end add_to_serializer(:topic_view, :assignment_status, false) { object.topic.assignment.status } add_to_serializer(:topic_view, :include_assignment_status?, false) do SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) && object.topic.assignment.present? end # SuggestedTopic serializer add_to_serializer(:suggested_topic, :assigned_to_user, false) do DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) end add_to_serializer(:suggested_topic, :include_assigned_to_user?) do (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(User) end add_to_serializer(:suggested_topic, :assigned_to_group, false) do DiscourseAssign::Helpers.build_assigned_to_group(object.assigned_to, object) end add_to_serializer(:suggested_topic, :include_assigned_to_group?) do (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group) end add_to_serializer(:suggested_topic, :indirectly_assigned_to) do DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object) end add_to_serializer(:suggested_topic, :include_indirectly_assigned_to?) do (SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present? end # TopicListItem serializer add_to_serializer(:topic_list_item, :indirectly_assigned_to) do DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object) end add_to_serializer(:topic_list_item, :include_indirectly_assigned_to?) do (SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present? end add_to_serializer(:topic_list_item, :assigned_to_user) do BasicUserSerializer.new(object.assigned_to, scope: scope, root: false).as_json end add_to_serializer(:topic_list_item, :include_assigned_to_user?) do (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(User) end add_to_serializer(:topic_list_item, :assigned_to_group) do AssignedGroupSerializer.new(object.assigned_to, scope: scope, root: false).as_json end add_to_serializer(:topic_list_item, :include_assigned_to_group?) do (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group) end add_to_serializer(:topic_list_item, :assignment_status, false) { object.assignment.status } add_to_serializer(:topic_list_item, :include_assignment_status?, false) do SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) && object.assignment.present? end # SearchTopicListItem serializer add_to_serializer(:search_topic_list_item, :assigned_to_user, false) do DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) end add_to_serializer(:search_topic_list_item, "include_assigned_to_user?") do (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(User) end add_to_serializer(:search_topic_list_item, :assigned_to_group, false) do AssignedGroupSerializer.new(object.assigned_to, scope: scope, root: false).as_json end add_to_serializer(:search_topic_list_item, "include_assigned_to_group?") do (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group) end add_to_serializer(:search_topic_list_item, :indirectly_assigned_to) do DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object) end add_to_serializer(:search_topic_list_item, :include_indirectly_assigned_to?) do (SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present? end # TopicsBulkAction TopicsBulkAction.register_operation("assign") do if @user.can_assign? assign_user = User.find_by_username(@operation[:username]) topics.each { |topic| Assigner.new(topic, @user).assign(assign_user) } end end TopicsBulkAction.register_operation("unassign") do if @user.can_assign? topics.each { |topic| Assigner.new(topic, @user).unassign if guardian.can_assign? } end end register_permitted_bulk_action_parameter :username add_to_class(:user_bookmark_base_serializer, :assigned_to) do @assigned_to ||= bookmarkable_type == "Topic" ? bookmarkable.assigned_to : bookmarkable.topic.assigned_to end add_to_class(:user_bookmark_base_serializer, :can_have_assignment?) do %w[Post Topic].include?(bookmarkable_type) end add_to_serializer(:user_bookmark_base, :assigned_to_user, false) do return if !can_have_assignment? BasicUserSerializer.new(assigned_to, scope: scope, root: false).as_json end add_to_serializer(:user_bookmark_base, "include_assigned_to_user?") do return false if !can_have_assignment? (SiteSetting.assigns_public || scope.can_assign?) && assigned_to&.is_a?(User) end add_to_serializer(:user_bookmark_base, :assigned_to_group, false) do return if !can_have_assignment? AssignedGroupSerializer.new(assigned_to, scope: scope, root: false).as_json end add_to_serializer(:user_bookmark_base, "include_assigned_to_group?") do return false if !can_have_assignment? (SiteSetting.assigns_public || scope.can_assign?) && assigned_to&.is_a?(Group) end add_to_serializer(:basic_user, :assign_icon) { "user-plus" } add_to_serializer(:basic_user, :assign_path) do return if !object.is_a?(User) SiteSetting.assigns_user_url_path.gsub("{username}", object.username) end # PostSerializer add_to_serializer(:post, :assigned_to_user) do BasicUserSerializer.new(object.assignment.assigned_to, scope: scope, root: false).as_json end add_to_serializer(:post, "include_assigned_to_user?") do (SiteSetting.assigns_public || scope.can_assign?) && object.assignment&.assigned_to&.is_a?(User) && object.assignment.active end add_to_serializer(:post, :assigned_to_group, false) do AssignedGroupSerializer.new(object.assignment.assigned_to, scope: scope, root: false).as_json end add_to_serializer(:post, "include_assigned_to_group?") do (SiteSetting.assigns_public || scope.can_assign?) && object.assignment&.assigned_to&.is_a?(Group) && object.assignment.active end add_to_serializer(:post, :assignment_note, false) { object.assignment.note } add_to_serializer(:post, :include_assignment_note?, false) do (SiteSetting.assigns_public || scope.can_assign?) && object.assignment.present? end add_to_serializer(:post, :assignment_status, false) { object.assignment.status } add_to_serializer(:post, :include_assignment_status?, false) do SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) && object.assignment.present? end # CurrentUser serializer add_to_serializer(:current_user, :can_assign) { object.can_assign? } # FlaggedTopic serializer add_to_serializer(:flagged_topic, :assigned_to_user) do DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) end add_to_serializer(:flagged_topic, :include_assigned_to_user?) do object.assigned_to && object.assigned_to&.is_a?(User) end add_to_serializer(:flagged_topic, :assigned_to_group) do DiscourseAssign::Helpers.build_assigned_to_group(object.assigned_to, object) end add_to_serializer(:flagged_topic, :include_assigned_to_group?) do object.assigned_to && object.assigned_to&.is_a?(Group) end # Reviewable add_custom_reviewable_filter( [ :assigned_to, Proc.new do |results, value| results.joins(<<~SQL).where(target_type: Post.name).where("u.username = ?", value) INNER JOIN posts p ON p.id = target_id INNER JOIN topics t ON t.id = p.topic_id INNER JOIN assignments a ON a.topic_id = t.id AND a.assigned_to_type = 'User' INNER JOIN users u ON u.id = a.assigned_to_id SQL end, ], ) # TopicTrackingState add_class_method(:topic_tracking_state, :publish_assigned_private_message) do |topic, assignee| return unless topic.private_message? opts = (assignee.is_a?(User) ? { user_ids: [assignee.id] } : { group_ids: [assignee.id] }) MessageBus.publish("/private-messages/assigned", { topic_id: topic.id }, opts) end # Event listeners on(:post_created) { |post| ::Assigner.auto_assign(post, force: true) } on(:post_edited) { |post, topic_changed| ::Assigner.auto_assign(post, force: true) } on(:topic_status_updated) do |topic, status, enabled| if SiteSetting.unassign_on_close && (status == "closed" || status == "autoclosed") && enabled && Assignment.exists?(topic_id: topic.id, active: true) assigner = ::Assigner.new(topic, Discourse.system_user) assigner.unassign(silent: true, deactivate: true) topic .posts .joins(:assignment) .find_each do |post| assigner = ::Assigner.new(post, Discourse.system_user) assigner.unassign(silent: true, deactivate: true) end MessageBus.publish("/topic/#{topic.id}", reload_topic: true, refresh_stream: true) end if SiteSetting.reassign_on_open && (status == "closed" || status == "autoclosed") && !enabled && Assignment.exists?(topic_id: topic.id, active: false) Assignment.where(topic_id: topic.id, target_type: "Topic").update_all(active: true) Assignment .where(topic_id: topic.id, target_type: "Post") .joins("INNER JOIN posts ON posts.id = target_id AND posts.deleted_at IS NULL") .update_all(active: true) MessageBus.publish("/topic/#{topic.id}", reload_topic: true, refresh_stream: true) end end on(:post_destroyed) do |post| if Assignment.exists?(target_type: "Post", target_id: post.id, active: true) Assignment.where(target_type: "Post", target_id: post.id).update_all(active: false) MessageBus.publish("/topic/#{post.topic_id}", reload_topic: true, refresh_stream: true) end # small actions have to be destroyed as link is incorrect PostCustomField .where(name: "action_code_post_id", value: post.id) .find_each do |post_custom_field| next if post_custom_field.post == nil if ![Post.types[:small_action], Post.types[:whisper]].include?( post_custom_field.post.post_type, ) next end post_custom_field.post.destroy end end on(:post_recovered) do |post| if SiteSetting.reassign_on_open && Assignment.where(target_type: "Post", target_id: post.id, active: false) Assignment.where(target_type: "Post", target_id: post.id).update_all(active: true) MessageBus.publish("/topic/#{post.topic_id}", reload_topic: true, refresh_stream: true) end end on(:move_to_inbox) do |info| topic = info[:topic] if topic.assignment TopicTrackingState.publish_assigned_private_message(topic, topic.assignment.assigned_to) end next if !SiteSetting.unassign_on_group_archive next if !info[:group] Assignment .where(topic_id: topic.id, active: false) .find_each do |assignment| next unless assignment.target assignment.update!(active: true) Jobs.enqueue( :assign_notification, topic_id: topic.id, post_id: assignment.target_type.is_a?(Topic) ? topic.first_post.id : assignment.target.id, assigned_to_id: assignment.assigned_to_id, assigned_to_type: assignment.assigned_to_type, assigned_by_id: assignment.assigned_by_user_id, skip_small_action_post: true, assignment_id: assignment.id, ) end end on(:archive_message) do |info| topic = info[:topic] next if !topic.assignment TopicTrackingState.publish_assigned_private_message(topic, topic.assignment.assigned_to) next if !SiteSetting.unassign_on_group_archive next if !info[:group] Assignment .where(topic_id: topic.id, active: true) .find_each do |assignment| assignment.update!(active: false) Jobs.enqueue( :unassign_notification, topic_id: topic.id, assigned_to_id: assignment.assigned_to.id, assigned_to_type: assignment.assigned_to_type, ) end end on(:user_removed_from_group) do |user, group| assign_allowed_groups = SiteSetting.assign_allowed_on_groups.split("|").map(&:to_i) if assign_allowed_groups.include?(group.id) groups = GroupUser.where(user: user).pluck(:group_id) if (groups & assign_allowed_groups).empty? topics = Topic.joins(:assignment).where("assignments.assigned_to_id = ?", user.id) topics.each { |topic| Assigner.new(topic, Discourse.system_user).unassign } end end end on(:post_moved) do |post, original_topic_id| assignment = Assignment.where(topic_id: original_topic_id, target_type: "Post", target_id: post.id).first next if !assignment if post.is_first_post? assignment.update!(topic_id: post.topic_id, target_type: "Topic", target_id: post.topic_id) else assignment.update!(topic_id: post.topic_id) end end class ::WebHook def self.enqueue_assign_hooks(event, payload) WebHook.enqueue_hooks(:assign, event, payload: payload) if active_web_hooks("assign").exists? end end register_search_advanced_filter(/in:assigned/) do |posts| posts.where(<<~SQL) if @guardian.can_assign? topics.id IN ( SELECT a.topic_id FROM assignments a WHERE a.active ) SQL end register_search_advanced_filter(/in:unassigned/) do |posts| posts.where(<<~SQL) if @guardian.can_assign? topics.id NOT IN ( SELECT a.topic_id FROM assignments a WHERE a.active ) SQL end register_search_advanced_filter(/assigned:(.+)$/) do |posts, match| if @guardian.can_assign? if user_id = User.find_by_username(match)&.id posts.where(<<~SQL, user_id) topics.id IN ( SELECT a.topic_id FROM assignments a WHERE a.assigned_to_id = ? AND a.assigned_to_type = 'User' AND a.active ) SQL elsif group_id = Group.find_by_name(match)&.id posts.where(<<~SQL, group_id) topics.id IN ( SELECT a.topic_id FROM assignments a WHERE a.assigned_to_id = ? AND a.assigned_to_type = 'Group' AND a.active ) SQL end end end if defined?(DiscourseAutomation) require "random_assign_utils" add_automation_scriptable("random_assign") do field :assignees_group, component: :group, required: true field :assigned_topic, component: :text, required: true field :minimum_time_between_assignments, component: :text field :max_recently_assigned_days, component: :text field :min_recently_assigned_days, component: :text field :skip_new_users_for_days, component: :text field :in_working_hours, component: :boolean field :post_template, component: :post version 1 triggerables %i[point_in_time recurring] script do |context, fields, automation| RandomAssignUtils.automation_script!(context, fields, automation) end end end end