mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Currently the /u/username/activity/assigns list sorts by topic bump date for assignments, whereas the user menu for assigns list sorts by read/unread and then the created_at date for the notification. The latter doesn't make much sense, most of the time when you are working on assignments the ones you are working on now which are being bumped somewhat frequently are the ones you want to see first, not in created_at order. This commit keeps the unread assignments at the top of the list, but sorts the rest by topic bump date.
1168 lines
39 KiB
Ruby
Vendored
1168 lines
39 KiB
Ruby
Vendored
# 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/discourse_assign/assignment_permissions"
|
|
require_relative "lib/discourse_assign/guardian_extensions"
|
|
require_relative "lib/validators/assign_statuses_validator"
|
|
|
|
after_initialize do
|
|
if respond_to?(:register_discourse_workflows_node)
|
|
register_discourse_workflows_node do
|
|
require_relative "lib/discourse_workflows/nodes/assign_topic/v1"
|
|
require_relative "lib/discourse_workflows/nodes/assigned/v1"
|
|
[DiscourseWorkflows::Nodes::AssignTopic::V1, DiscourseWorkflows::Nodes::Assigned::V1]
|
|
end
|
|
|
|
on(:assigned) do |assignment|
|
|
require_relative "lib/discourse_workflows/nodes/assigned/v1"
|
|
DiscourseWorkflows::EventListener.handle(DiscourseWorkflows::Nodes::Assigned::V1, assignment)
|
|
end
|
|
end
|
|
UserUpdater::OPTION_ATTR.push(:notification_level_when_assigned)
|
|
|
|
reloadable_patch do |plugin|
|
|
Guardian.prepend(DiscourseAssign::GuardianExtensions)
|
|
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(
|
|
:notification,
|
|
:topic_bumped_at,
|
|
include_condition: -> do
|
|
object.notification_type == Notification.types[:assigned] && object.topic.present?
|
|
end,
|
|
) { object.topic.bumped_at }
|
|
|
|
add_to_serializer(
|
|
:group_show,
|
|
:assignment_count,
|
|
include_condition: -> { scope.can_assign_globally? },
|
|
) 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 = value.to_i if 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 |target = nil|
|
|
if target
|
|
guardian.can_assign?(target)
|
|
else
|
|
return @can_assign if defined?(@can_assign)
|
|
|
|
@can_assign = guardian.can_assign?
|
|
end
|
|
end
|
|
|
|
add_to_class(:user, :can_assign_globally?) do
|
|
return @can_assign_globally if defined?(@can_assign_globally)
|
|
|
|
@can_assign_globally = guardian.can_assign_globally?
|
|
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?) { assignable_level > Group::ALIAS_LEVELS[:nobody] }
|
|
|
|
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_category_custom_field_type(
|
|
DiscourseAssign::AssignmentPermissions::CATEGORY_ADDITIONAL_ASSIGN_ALLOWED_GROUPS,
|
|
:string,
|
|
)
|
|
register_preloaded_category_custom_fields("enable_unassigned_filter")
|
|
register_preloaded_category_custom_fields(
|
|
DiscourseAssign::AssignmentPermissions::CATEGORY_ADDITIONAL_ASSIGN_ALLOWED_GROUPS,
|
|
)
|
|
|
|
BookmarkQuery.on_preload do |bookmarks, _bookmark_query|
|
|
if SiteSetting.assign_enabled?
|
|
topics = Bookmark.select_type(bookmarks, "Topic").map(&:bookmarkable)
|
|
posts = Bookmark.select_type(bookmarks, "Post").map(&:bookmarkable)
|
|
|
|
assignments =
|
|
Assignment
|
|
.strict_loading
|
|
.active
|
|
.where(topic_id: topics.map(&:id).concat(posts.map(&:topic_id)).uniq)
|
|
.includes(:assigned_to)
|
|
|
|
topic_assignments, post_assignments = assignments.partition { it.target_type == "Topic" }
|
|
topic_assignments_map = topic_assignments.index_by(&:target_id)
|
|
post_assignments_map = post_assignments.index_by(&:target_id)
|
|
|
|
topics.each do |topic|
|
|
assignment = topic_assignments_map[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
|
|
|
|
posts.each do |post|
|
|
assignment = post_assignments_map[post.id]
|
|
post.preload_assigned_to(assignment&.assigned_to)
|
|
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_globally? && !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 guardian.can_assign_globally?
|
|
|
|
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_and_members!(group)
|
|
|
|
raise Discourse::NotFound unless group
|
|
raise Discourse::InvalidAccess unless guardian.can_assign_globally?
|
|
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
|
|
|
|
add_to_class(:post, :assigned_to) do
|
|
return @assigned_to if defined?(@assigned_to)
|
|
@assigned_to = assignment.assigned_to if assignment&.active
|
|
end
|
|
|
|
add_to_class(:post, :preload_assigned_to) { |assigned_to| @assigned_to = assigned_to }
|
|
|
|
# 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_globally? || 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,
|
|
:can_assign,
|
|
include_condition: -> { scope.current_user.present? },
|
|
) { scope.can_assign?(object.topic) }
|
|
|
|
add_to_serializer(
|
|
:topic_view,
|
|
:assigned_to_user,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?(object.topic)) &&
|
|
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)) &&
|
|
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)) &&
|
|
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)) &&
|
|
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)) &&
|
|
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)) && 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)) && 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)) &&
|
|
object.indirectly_assigned_to.present?
|
|
end,
|
|
) { DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object) }
|
|
|
|
add_to_serializer(
|
|
:topic_list_item,
|
|
:can_assign,
|
|
include_condition: -> { scope.current_user.present? },
|
|
) { scope.can_assign?(object) }
|
|
|
|
# TopicListItem serializer
|
|
add_to_serializer(
|
|
:topic_list_item,
|
|
:indirectly_assigned_to,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?(object)) &&
|
|
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)) && 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)) && 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)) &&
|
|
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)) && 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)) && 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)) &&
|
|
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|
|
|
next if !@user.can_assign?(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?(topic) }
|
|
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.assigned_to
|
|
end
|
|
|
|
add_to_class(:user_bookmark_base_serializer, :can_have_assignment?) do
|
|
%w[Post Topic].include?(bookmarkable_type)
|
|
end
|
|
|
|
add_to_class(:user_bookmark_base_serializer, :assignment_target) do
|
|
bookmarkable_type == "Topic" ? bookmarkable : bookmarkable.topic
|
|
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?(assignment_target)) &&
|
|
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?(assignment_target)) &&
|
|
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, :can_assign, include_condition: -> { scope.current_user.present? }) do
|
|
scope.can_assign?(object)
|
|
end
|
|
|
|
add_to_serializer(
|
|
:post,
|
|
:assigned_to_user,
|
|
include_condition: -> do
|
|
(SiteSetting.assigns_public || scope.can_assign?(object)) &&
|
|
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)) &&
|
|
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)) && 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)) && object.assignment.present?
|
|
end,
|
|
) { object.assignment.status }
|
|
|
|
# CurrentUser serializer
|
|
add_to_serializer(:current_user, :can_assign) { object.can_assign? }
|
|
add_to_serializer(:current_user, :can_assign_globally) { object.can_assign_globally? }
|
|
|
|
# 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_globally? || 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_globally?
|
|
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_modifier(:posts_filter_options) do |results, guardian|
|
|
if SiteSetting.assign_enabled && (guardian.can_assign? || SiteSetting.assigns_public)
|
|
results << {
|
|
name: "assigned_to:",
|
|
description: I18n.t("discourse_assign.filter.description.assigned"),
|
|
type: "username",
|
|
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
|
|
|
|
PostsFilter.add_filter("assigned_to") do |scope, filter_values, guardian|
|
|
if !SiteSetting.assign_enabled || !(guardian.can_assign? || SiteSetting.assigns_public)
|
|
raise Discourse::InvalidAccess.new(
|
|
"Assigns are not enabled or you do not have permission to see assigns.",
|
|
)
|
|
end
|
|
|
|
names =
|
|
filter_values.compact.flat_map { |value| value.to_s.split(",") }.map(&:strip).reject(&:blank?)
|
|
next scope if names.blank?
|
|
|
|
next scope.where.not(topic_id: Assignment.active.select(:topic_id)) if names.include?("nobody")
|
|
|
|
next scope.where(topic_id: Assignment.active.select(:topic_id)) if names.include?("*")
|
|
|
|
user_ids = User.where(username_lower: names.map(&:downcase)).select(:id)
|
|
scope.where(
|
|
topic_id:
|
|
Assignment
|
|
.active
|
|
.where(assigned_to_type: "User", assigned_to_id: user_ids)
|
|
.select(:topic_id),
|
|
)
|
|
end
|
|
|
|
register_search_advanced_filter(/in:assigned/) do |posts|
|
|
next if !@guardian.can_assign_globally?
|
|
|
|
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_globally?
|
|
|
|
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_globally? || 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
|
|
.visible_groups(@guardian.user)
|
|
.members_visible_groups(@guardian.user)
|
|
.where(name: match)
|
|
.pick(: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?
|
|
next if post.topic.reload.solved.present?
|
|
|
|
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
|