mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-02 06:48:58 +08:00
The Assignments tab on group pages had two problematic visibility conditions that caused it to disappear unexpectedly: 1. `can_show_assigned_tab?` checked whether ALL members of the group belonged to an `assign_allowed_on_groups` group. This meant a single member outside those groups would hide the tab for everyone — even when the group was fully assignable and had active assignments. 2. The frontend required `assignment_count > 0`, so the tab silently vanished when the last assignment was resolved, with no indication of whether the feature was unconfigured or simply empty. The root cause is that tab visibility was answering the wrong question: "can all members use the assign feature?" instead of "is this group set up to receive assignments?" This replaces the member-overlap SQL in `can_show_assigned_tab?` with a simple check on `assignable_level > nobody` — the setting that actually controls whether a group can receive assignments. The `assignment_count > 0` gate is removed from the frontend so the tab stays visible with an empty state, consistent with other group tabs. Also fixes the `assign_allowed_on_groups` setting description which incorrectly stated it controls who can be assigned topics (that's `assignable_level`). Ref - t/179679
1036 lines
34 KiB
Ruby
1036 lines
34 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# name: discourse-assign
|
|
# about: Provides the ability to assign topics and individual posts to a user or group.
|
|
# meta_topic_id: 58044
|
|
# version: 1.0.1
|
|
# authors: Sam Saffron
|
|
# url: https://github.com/discourse/discourse/tree/main/plugins/discourse-assign
|
|
|
|
enabled_site_setting :assign_enabled
|
|
|
|
register_asset "stylesheets/assigns.scss"
|
|
register_asset "stylesheets/mobile/assigns.scss", :mobile
|
|
|
|
%w[user-plus user-xmark group-plus group-times].each { |i| register_svg_icon(i) }
|
|
|
|
module ::DiscourseAssign
|
|
PLUGIN_NAME = "discourse-assign"
|
|
end
|
|
|
|
require_relative "lib/discourse_assign/engine"
|
|
require_relative "lib/validators/assign_statuses_validator"
|
|
|
|
after_initialize do
|
|
UserUpdater::OPTION_ATTR.push(:notification_level_when_assigned)
|
|
|
|
reloadable_patch do |plugin|
|
|
Group.prepend(DiscourseAssign::GroupExtension)
|
|
ListController.prepend(DiscourseAssign::ListControllerExtension)
|
|
Post.prepend(DiscourseAssign::PostExtension)
|
|
Topic.prepend(DiscourseAssign::TopicExtension)
|
|
WebHook.prepend(DiscourseAssign::WebHookExtension)
|
|
Notification.prepend(DiscourseAssign::NotificationExtension)
|
|
UserOption.prepend(DiscourseAssign::UserOptionExtension)
|
|
end
|
|
|
|
add_to_serializer(:user_option, :notification_level_when_assigned) do
|
|
object.notification_level_when_assigned
|
|
end
|
|
|
|
add_to_serializer(:current_user_option, :notification_level_when_assigned) do
|
|
object.notification_level_when_assigned
|
|
end
|
|
|
|
register_group_param(:assignable_level)
|
|
register_groups_callback_for_users_search_controller_action(:assignable_groups) do |groups, user|
|
|
groups.assignable(user)
|
|
end
|
|
|
|
frequency_field = PendingAssignsReminder::REMINDERS_FREQUENCY
|
|
register_editable_user_custom_field frequency_field
|
|
register_user_custom_field_type(frequency_field, :integer, max_length: 10)
|
|
DiscoursePluginRegistry.serialized_current_user_fields << frequency_field
|
|
add_to_serializer(:user, :reminders_frequency) { RemindAssignsFrequencySiteSettings.values }
|
|
|
|
add_to_serializer(:group_show, :assignment_count, include_condition: -> { scope.can_assign? }) 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, :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
|
|
return @can_assign if defined?(@can_assign)
|
|
|
|
allowed_groups = SiteSetting.assign_allowed_on_groups_map
|
|
@can_assign = admin? || (allowed_groups.present? && groups.where(id: allowed_groups).exists?)
|
|
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
|
|
self.assignable_level > Group::ALIAS_LEVELS[:nobody]
|
|
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 }
|
|
|
|
register_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|
|
|
assignment = assignments[topic.id]
|
|
# NOTE: preloading to `nil` is necessary to avoid N+1 queries
|
|
topic.preload_assigned_to(assignment&.assigned_to)
|
|
topic.preload_assignment_status(assignment&.status)
|
|
end
|
|
end
|
|
end
|
|
|
|
TopicView.on_preload do |topic_view|
|
|
if SiteSetting.assign_enabled
|
|
topic_view.instance_variable_set(:@posts, topic_view.posts.includes(:assignment))
|
|
end
|
|
end
|
|
|
|
TopicList.on_preload do |topics, topic_list|
|
|
next unless SiteSetting.assign_enabled?
|
|
|
|
can_assign = topic_list.current_user&.can_assign?
|
|
allowed_access = SiteSetting.assigns_public || can_assign
|
|
|
|
next if !allowed_access || topics.empty?
|
|
|
|
assignments =
|
|
Assignment.strict_loading.active.where(topic: topics).includes(:target, :assigned_to)
|
|
assignments_map = assignments.group_by(&:topic_id)
|
|
|
|
user_ids = assignments.filter(&: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(&:assigned_to_group?).map(&:assigned_to_id)
|
|
groups_map = Group.where(id: group_ids).index_by(&:id)
|
|
|
|
topics.each do |topic|
|
|
if assignments = assignments_map[topic.id]
|
|
topic_assignments, post_assignments = assignments.partition { it.target_type == "Topic" }
|
|
|
|
direct_assignment = topic_assignments.find { it.target_id == topic.id }
|
|
|
|
indirectly_assigned_to = {}
|
|
|
|
post_assignments.each do |assignment|
|
|
next unless assignment.target
|
|
|
|
if assignment.assigned_to_user?
|
|
indirectly_assigned_to[assignment.target_id] = {
|
|
assigned_to: users_map[assignment.assigned_to_id],
|
|
post_number: assignment.target.post_number,
|
|
}
|
|
elsif assignment.assigned_to_group?
|
|
indirectly_assigned_to[assignment.target_id] = {
|
|
assigned_to: groups_map[assignment.assigned_to_id],
|
|
post_number: assignment.target.post_number,
|
|
}
|
|
end
|
|
end
|
|
|
|
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
|
|
end
|
|
|
|
# NOTE: preloading to `nil` is necessary to avoid N+1 queries
|
|
topic.preload_assigned_to(assigned_to)
|
|
topic.preload_assignment_status(direct_assignment&.status)
|
|
topic.preload_indirectly_assigned_to(indirectly_assigned_to)
|
|
end
|
|
end
|
|
|
|
Search.on_preload do |results, search|
|
|
next unless SiteSetting.assign_enabled?
|
|
|
|
can_assign = search.guardian&.can_assign?
|
|
allowed_access = SiteSetting.assigns_public || can_assign
|
|
|
|
next if !allowed_access || results.posts.empty?
|
|
|
|
topics = results.posts.map(&:topic)
|
|
|
|
assignments =
|
|
Assignment
|
|
.strict_loading
|
|
.active
|
|
.where(topic: topics)
|
|
.includes(:assigned_to, :target)
|
|
.group_by(&:topic_id)
|
|
|
|
results.posts.each do |post|
|
|
if topic_assignments = assignments[post.topic_id]
|
|
direct_assignment = topic_assignments.find { it.target_type == "Topic" }
|
|
indirect_assignments = topic_assignments.select { it.target_type == "Post" }
|
|
end
|
|
|
|
if indirect_assignments.present?
|
|
indirect_assignment_map = {}
|
|
|
|
indirect_assignments.each do |assignment|
|
|
next unless assignment.target
|
|
indirect_assignment_map[assignment.target_id] = {
|
|
assigned_to: assignment.assigned_to,
|
|
post_number: assignment.target.post_number,
|
|
}
|
|
end
|
|
end
|
|
|
|
# NOTE: preloading to `nil` is necessary to avoid N+1 queries
|
|
post.topic.preload_assigned_to(direct_assignment&.assigned_to)
|
|
post.topic.preload_assignment_status(direct_assignment&.status)
|
|
post.topic.preload_indirectly_assigned_to(indirect_assignment_map)
|
|
end
|
|
end
|
|
|
|
# TopicQuery
|
|
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.not(a: { assigned_to_id: nil })
|
|
)
|
|
end
|
|
|
|
user_id = topic_query.guardian.user.id if name == "me"
|
|
user_id ||= User.where(username_lower: name.downcase).pick(: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
|
|
.visible_groups(topic_query.guardian.user)
|
|
.members_visible_groups(topic_query.guardian.user)
|
|
.where(name: name.downcase)
|
|
.pick(: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_pms: true)
|
|
|
|
assignee_condition = "(a.assigned_to_id = :group_id AND a.assigned_to_type = 'Group')"
|
|
if @options[:filter] != :direct
|
|
assignee_condition +=
|
|
" OR (a.assigned_to_id IN (SELECT user_id from group_users where group_id = :group_id) AND a.assigned_to_type = 'User')"
|
|
end
|
|
|
|
topic_ids_sql = <<~SQL
|
|
SELECT a.topic_id FROM assignments a
|
|
LEFT JOIN topics t ON t.id = a.topic_id
|
|
LEFT JOIN posts p ON p.id = a.target_id AND a.target_type = 'Post'
|
|
WHERE a.active
|
|
AND t.deleted_at IS NULL
|
|
AND (
|
|
a.target_type = 'Topic' OR
|
|
(a.target_type = 'Post' AND p.deleted_at IS NULL AND p.deleted_by_id IS NULL AND p.user_deleted = false)
|
|
)
|
|
AND (#{assignee_condition})
|
|
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
|
|
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, :assignment_status) do
|
|
return @assignment_status if defined?(@assignment_status)
|
|
@assignment_status = assignment.status if SiteSetting.enable_assign_status && 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_assignment_status) do |assignment_status|
|
|
@assignment_status = assignment_status
|
|
end
|
|
|
|
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,
|
|
include_condition: -> 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,
|
|
) do
|
|
TopicQuery
|
|
.new(object.current_user, guardian: scope, limit: false)
|
|
.private_messages_assigned_query(object.current_user)
|
|
.count
|
|
end
|
|
|
|
# TopicView serializer
|
|
add_to_serializer(
|
|
:topic_view,
|
|
:assigned_to_user,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to.is_a?(User)
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_assigned_to_user(object.topic.assigned_to, object.topic) }
|
|
|
|
add_to_serializer(
|
|
:topic_view,
|
|
:assigned_to_group,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to.is_a?(Group)
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_assigned_to_group(object.topic.assigned_to, object.topic) }
|
|
|
|
add_to_serializer(
|
|
:topic_view,
|
|
:indirectly_assigned_to,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) &&
|
|
object.topic.indirectly_assigned_to.present?
|
|
end,
|
|
) do
|
|
DiscourseAssign::Helpers.build_indirectly_assigned_to(
|
|
object.topic.indirectly_assigned_to,
|
|
object.topic,
|
|
)
|
|
end
|
|
|
|
add_to_serializer(
|
|
:topic_view,
|
|
:assignment_note,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assignment.present?
|
|
end,
|
|
) { object.topic.assignment.note }
|
|
|
|
add_to_serializer(
|
|
:topic_view,
|
|
:assignment_status,
|
|
include_condition: -> do
|
|
SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) &&
|
|
object.topic.assignment_status.present?
|
|
end,
|
|
) { object.topic.assignment_status }
|
|
|
|
# SuggestedTopic serializer
|
|
add_to_serializer(
|
|
:suggested_topic,
|
|
:assigned_to_user,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(User)
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) }
|
|
|
|
add_to_serializer(
|
|
:suggested_topic,
|
|
:assigned_to_group,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(Group)
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_assigned_to_group(object.assigned_to, object) }
|
|
|
|
add_to_serializer(
|
|
:suggested_topic,
|
|
:indirectly_assigned_to,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present?
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object) }
|
|
|
|
# TopicListItem serializer
|
|
add_to_serializer(
|
|
:topic_list_item,
|
|
:indirectly_assigned_to,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present?
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object) }
|
|
|
|
add_to_serializer(
|
|
:topic_list_item,
|
|
:assigned_to_user,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(User)
|
|
end,
|
|
) { BasicUserSerializer.new(object.assigned_to, scope: scope, root: false).as_json }
|
|
|
|
add_to_serializer(
|
|
:topic_list_item,
|
|
:assigned_to_group,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(Group)
|
|
end,
|
|
) { AssignedGroupSerializer.new(object.assigned_to, scope: scope, root: false).as_json }
|
|
|
|
add_to_serializer(
|
|
:topic_list_item,
|
|
:assignment_status,
|
|
include_condition: -> do
|
|
SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) &&
|
|
object.assignment_status.present?
|
|
end,
|
|
) { object.assignment_status }
|
|
|
|
# SearchTopicListItem serializer
|
|
add_to_serializer(
|
|
:search_topic_list_item,
|
|
:assigned_to_user,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(User)
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) }
|
|
|
|
add_to_serializer(
|
|
:search_topic_list_item,
|
|
:assigned_to_group,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to.is_a?(Group)
|
|
end,
|
|
) { AssignedGroupSerializer.new(object.assigned_to, scope: scope, root: false).as_json }
|
|
|
|
add_to_serializer(
|
|
:search_topic_list_item,
|
|
:indirectly_assigned_to,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present?
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object) }
|
|
|
|
# TopicsBulkAction
|
|
TopicsBulkAction.register_operation("assign") do
|
|
if @user.can_assign?
|
|
assign_user = User.find_by_username(@operation[:username])
|
|
topics.each do |topic|
|
|
Assigner.new(topic, @user).assign(
|
|
assign_user,
|
|
status: @operation[:status],
|
|
note: @operation[:note],
|
|
)
|
|
end
|
|
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
|
|
register_permitted_bulk_action_parameter :status
|
|
register_permitted_bulk_action_parameter :note
|
|
|
|
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,
|
|
include_condition: -> do
|
|
return false if !can_have_assignment?
|
|
(SiteSetting.assigns_public || scope.can_assign?) && assigned_to.is_a?(User)
|
|
end,
|
|
) do
|
|
return if !can_have_assignment?
|
|
BasicUserSerializer.new(assigned_to, scope: scope, root: false).as_json
|
|
end
|
|
|
|
add_to_serializer(
|
|
:user_bookmark_base,
|
|
:assigned_to_group,
|
|
include_condition: -> do
|
|
return false if !can_have_assignment?
|
|
(SiteSetting.assigns_public || scope.can_assign?) && assigned_to.is_a?(Group)
|
|
end,
|
|
) do
|
|
return if !can_have_assignment?
|
|
AssignedGroupSerializer.new(assigned_to, scope: scope, root: false).as_json
|
|
end
|
|
|
|
# PostSerializer
|
|
add_to_serializer(
|
|
:post,
|
|
:assigned_to_user,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) &&
|
|
object.assignment&.assigned_to.is_a?(User) && object.assignment.active
|
|
end,
|
|
) { BasicUserSerializer.new(object.assignment.assigned_to, scope: scope, root: false).as_json }
|
|
|
|
add_to_serializer(
|
|
:post,
|
|
:assigned_to_group,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) &&
|
|
object.assignment&.assigned_to.is_a?(Group) && object.assignment.active
|
|
end,
|
|
) do
|
|
AssignedGroupSerializer.new(object.assignment.assigned_to, scope: scope, root: false).as_json
|
|
end
|
|
|
|
add_to_serializer(
|
|
:post,
|
|
:assignment_note,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?) && object.assignment.present?
|
|
end,
|
|
) { object.assignment.note }
|
|
|
|
add_to_serializer(
|
|
:post,
|
|
:assignment_status,
|
|
include_condition: -> do
|
|
SiteSetting.enable_assign_status && (SiteSetting.assigns_public || scope.can_assign?) &&
|
|
object.assignment.present?
|
|
end,
|
|
) { object.assignment.status }
|
|
|
|
# CurrentUser serializer
|
|
add_to_serializer(:current_user, :can_assign) { object.can_assign? }
|
|
|
|
# FlaggedTopic serializer
|
|
add_to_serializer(
|
|
:flagged_topic,
|
|
:assigned_to_user,
|
|
include_condition: -> { object.assigned_to && object.assigned_to.is_a?(User) },
|
|
) { DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) }
|
|
|
|
add_to_serializer(
|
|
:flagged_topic,
|
|
:assigned_to_group,
|
|
include_condition: -> { object.assigned_to && object.assigned_to.is_a?(Group) },
|
|
) { DiscourseAssign::Helpers.build_assigned_to_group(object.assigned_to, object) }
|
|
|
|
# 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.active.exists?(topic: topic)
|
|
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.inactive.exists?(topic: topic)
|
|
Assignment.reactivate!(topic: topic)
|
|
MessageBus.publish("/topic/#{topic.id}", reload_topic: true, refresh_stream: true)
|
|
end
|
|
end
|
|
|
|
on(:post_destroyed) do |post|
|
|
if Assignment.active.exists?(target: post)
|
|
post.assignment.deactivate!
|
|
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.inactive.exists?(target: post)
|
|
post.assignment.reactivate!
|
|
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.reactivate!(topic: topic)
|
|
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.deactivate!(topic: topic)
|
|
end
|
|
|
|
on(:user_added_to_group) do |user, group, automatic:|
|
|
group.assignments.active.find_each do |assignment|
|
|
Jobs.enqueue(:assign_notification, assignment_id: assignment.id)
|
|
end
|
|
end
|
|
|
|
on(:user_removed_from_group) do |user, group|
|
|
user.notifications.for_assignment(group.assignments.select(:id)).destroy_all
|
|
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
|
|
|
|
on(:group_destroyed) do |group, user_ids|
|
|
User
|
|
.where(id: user_ids)
|
|
.find_each do |user|
|
|
user.notifications.for_assignment(group.assignments.select(:id)).destroy_all
|
|
end
|
|
|
|
Assignment.active_for_group(group).destroy_all
|
|
end
|
|
|
|
add_filter_custom_filter("assigned") do |scope, filter_values, guardian|
|
|
next if !guardian.can_assign? || filter_values.blank?
|
|
|
|
# Handle multiple comma-separated values (user1,group1,user2)
|
|
names =
|
|
filter_values.compact.flat_map { |value| value.to_s.split(",") }.map(&:strip).reject(&:blank?)
|
|
|
|
next if names.blank?
|
|
|
|
if names.include?("nobody")
|
|
next scope.where("topics.id NOT IN (SELECT a.topic_id FROM assignments a WHERE a.active)")
|
|
end
|
|
|
|
if names.include?("*")
|
|
next scope.where("topics.id IN (SELECT a.topic_id FROM assignments a WHERE a.active)")
|
|
end
|
|
|
|
found_names, user_ids =
|
|
User.where(username_lower: names.map(&:downcase)).pluck(:username, :id).transpose
|
|
|
|
found_names ||= []
|
|
user_ids ||= []
|
|
# a bit edge casey cause we have username_lower for users but not for groups
|
|
# we share a namespace though so in practice this is ok
|
|
remaining_names = names - found_names
|
|
group_ids = []
|
|
if remaining_names.present?
|
|
group_ids.concat(
|
|
Group
|
|
.visible_groups(guardian.user)
|
|
.members_visible_groups(guardian.user)
|
|
.where(name: remaining_names)
|
|
.pluck(:id),
|
|
)
|
|
end
|
|
|
|
next scope.none if user_ids.empty? && group_ids.empty?
|
|
|
|
assignment_query = Assignment.none # needed cause we are adding .or later
|
|
if user_ids.present?
|
|
assignment_query =
|
|
assignment_query.or(
|
|
Assignment.active.where(assigned_to_type: "User", assigned_to_id: user_ids),
|
|
)
|
|
end
|
|
|
|
if group_ids.present?
|
|
assignment_query =
|
|
assignment_query.or(
|
|
Assignment.active.where(assigned_to_type: "Group", assigned_to_id: group_ids),
|
|
)
|
|
end
|
|
|
|
scope.where(id: assignment_query.select(:topic_id))
|
|
end
|
|
|
|
register_modifier(:topics_filter_options) do |results, guardian|
|
|
if guardian.can_assign?
|
|
results << {
|
|
name: "assigned:",
|
|
description: I18n.t("discourse_assign.filter.description.assigned"),
|
|
type: "username_group_list",
|
|
extra_entries: [
|
|
{ name: "nobody", description: I18n.t("discourse_assign.filter.description.nobody") },
|
|
{ name: "*", description: I18n.t("discourse_assign.filter.description.anyone") },
|
|
],
|
|
priority: 1,
|
|
}
|
|
end
|
|
results
|
|
end
|
|
|
|
register_search_advanced_filter(/in:assigned/) do |posts|
|
|
next if !@guardian.can_assign?
|
|
|
|
posts.where("topics.id IN (SELECT a.topic_id FROM assignments a WHERE a.active)")
|
|
end
|
|
|
|
register_search_advanced_filter(/in:unassigned/) do |posts|
|
|
next if !@guardian.can_assign?
|
|
|
|
posts.where("topics.id NOT IN (SELECT a.topic_id FROM assignments a WHERE a.active)")
|
|
end
|
|
|
|
register_search_advanced_filter(/assigned:(.+)$/) do |posts, match|
|
|
next if !@guardian.can_assign? || match.blank?
|
|
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
|
|
|
|
if defined?(DiscourseAutomation)
|
|
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
|
|
|
|
if defined?(DiscourseSolved)
|
|
register_modifier(:assigns_reminder_assigned_topics_query) do |query|
|
|
next query if !SiteSetting.ignore_solved_topics_in_assigned_reminder
|
|
query.where.not(id: DiscourseSolved::SolvedTopic.select(:topic_id))
|
|
end
|
|
|
|
register_modifier(:assigned_count_for_user_query) do |query, user|
|
|
next query if !SiteSetting.ignore_solved_topics_in_assigned_reminder
|
|
next query if SiteSetting.assignment_status_on_solve.blank?
|
|
query.where.not(status: SiteSetting.assignment_status_on_solve)
|
|
end
|
|
|
|
on(:accepted_solution) do |post|
|
|
next if SiteSetting.assignment_status_on_solve.blank?
|
|
assignments = Assignment.includes(:target).where(topic: post.topic)
|
|
assignments.each do |assignment|
|
|
assigned_user = User.find_by(id: assignment.assigned_to_id)
|
|
Assigner.new(assignment.target, assigned_user).assign(
|
|
assigned_user,
|
|
status: SiteSetting.assignment_status_on_solve,
|
|
)
|
|
end
|
|
end
|
|
|
|
on(:unaccepted_solution) do |post|
|
|
next if SiteSetting.assignment_status_on_unsolve.blank?
|
|
assignments = Assignment.includes(:target).where(topic: post.topic)
|
|
assignments.each do |assignment|
|
|
assigned_user = User.find_by(id: assignment.assigned_to_id)
|
|
Assigner.new(assignment.target, assigned_user).assign(
|
|
assigned_user,
|
|
status: SiteSetting.assignment_status_on_unsolve,
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|