Skip to content

Commit fbd1fa3

Browse files
committed
FEATURE: Add a fallback to auto-assign
Currently, when the auto-assign logic can’t find a user to assign, it will fail saying there was no one to assign. The current logic is this one: - Don’t pick anyone who’s been picked in the last 180 days - If no one has been found, then try the same thing but only for the last 14 days. While this is working relatively well for large enough groups, it doesn’t work at all with very small groups (like 2 people) and it creates unnecessary noise. This patch addresses this issue by adding a fallback to the current logic. Now, if the two first rules fail, instead of saying that no one was assigned, we assign the least recently assigned person. This way, the logic will continue to work with large groups but will also work nicely with small groups.
1 parent 4ce9ae9 commit fbd1fa3

File tree

2 files changed

+360
-336
lines changed

2 files changed

+360
-336
lines changed

lib/random_assign_utils.rb

Lines changed: 158 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,190 @@
11
# frozen_string_literal: true
22

33
class RandomAssignUtils
4-
def self.raise_error(automation, message)
5-
raise("[discourse-automation id=#{automation.id}] #{message}.")
6-
end
4+
attr_reader :context, :fields, :automation, :topic, :group
75

8-
def self.log_info(automation, message)
9-
Rails.logger.info("[discourse-automation id=#{automation.id}] #{message}.")
6+
def self.automation_script!(...)
7+
new(...).automation_script!
108
end
119

12-
def self.automation_script!(context, fields, automation)
13-
raise_error(automation, "discourse-assign is not enabled") unless SiteSetting.assign_enabled?
10+
def initialize(context, fields, automation)
11+
@context = context
12+
@fields = fields
13+
@automation = automation
1414

15+
raise_error("discourse-assign is not enabled") unless SiteSetting.assign_enabled?
1516
unless topic_id = fields.dig("assigned_topic", "value")
16-
raise_error(automation, "`assigned_topic` not provided")
17-
end
18-
19-
unless topic = Topic.find_by(id: topic_id)
20-
raise_error(automation, "Topic(#{topic_id}) not found")
17+
raise_error("`assigned_topic` not provided")
2118
end
22-
23-
min_hours = fields.dig("minimum_time_between_assignments", "value").presence
24-
if min_hours &&
25-
TopicCustomField
26-
.where(name: "assigned_to_id", topic_id: topic_id)
27-
.where("created_at < ?", min_hours.to_i.hours.ago)
28-
.exists?
29-
log_info(automation, "Topic(#{topic_id}) has already been assigned recently")
30-
return
19+
unless @topic = Topic.find_by(id: topic_id)
20+
raise_error("Topic(#{topic_id}) not found")
3121
end
3222

3323
unless group_id = fields.dig("assignees_group", "value")
34-
raise_error(automation, "`assignees_group` not provided")
24+
raise_error("`assignees_group` not provided")
3525
end
36-
37-
unless group = Group.find_by(id: group_id)
38-
raise_error(automation, "Group(#{group_id}) not found")
26+
unless @group = Group.find_by(id: group_id)
27+
raise_error("Group(#{group_id}) not found")
3928
end
29+
end
4030

41-
assignable_user_ids = User.assign_allowed.pluck(:id)
42-
users_on_holiday =
43-
Set.new(
44-
User.where(
45-
id: UserCustomField.where(name: "on_holiday", value: "t").select(:user_id),
46-
).pluck(:id),
31+
def automation_script!
32+
return log_info("Topic(#{topic.id}) has already been assigned recently") if assigned_recently?
33+
return no_one! unless assigned_user
34+
assign_user!
35+
end
36+
37+
def recently_assigned_users_ids(from)
38+
usernames =
39+
PostCustomField
40+
.joins(:post)
41+
.where(
42+
name: "action_code_who",
43+
posts: {
44+
topic: topic,
45+
action_code: %w[assigned reassigned assigned_to_post],
46+
},
47+
)
48+
.where("posts.created_at > ?", from)
49+
.order("posts.created_at DESC")
50+
.pluck(:value)
51+
.uniq
52+
User
53+
.where(username: usernames)
54+
.joins(
55+
"JOIN unnest('{#{usernames.join(",")}}'::text[]) WITH ORDINALITY t(username, ord) USING(username)",
4756
)
57+
.limit(100)
58+
.order("ord")
59+
.pluck(:id)
60+
end
4861

49-
group_users = group.group_users.joins(:user)
50-
if skip_new_users_for_days = fields.dig("skip_new_users_for_days", "value").presence
51-
group_users = group_users.where("users.created_at < ?", skip_new_users_for_days.to_i.days.ago)
52-
end
62+
private
63+
64+
def assigned_user
65+
@assigned_user ||=
66+
begin
67+
group_users_ids = group_users.pluck(:id)
68+
return if group_users_ids.empty?
69+
70+
last_assignees_ids = recently_assigned_users_ids(max_recently_assigned_days)
71+
users_ids = group_users_ids - last_assignees_ids
72+
if users_ids.blank?
73+
recently_assigned_users_ids = recently_assigned_users_ids(min_recently_assigned_days)
74+
users_ids = group_users_ids - recently_assigned_users_ids
75+
end
76+
users_ids << last_assignees_ids.intersection(group_users_ids).last if users_ids.blank?
77+
if fields.dig("in_working_hours", "value")
78+
assign_to_user_id = users_ids.shuffle.detect { |user_id| in_working_hours?(user_id) }
79+
end
80+
assign_to_user_id ||= users_ids.sample
81+
82+
User.find(assign_to_user_id)
83+
end
84+
end
5385

54-
group_users_ids =
55-
group_users
56-
.pluck("users.id")
57-
.filter { |user_id| assignable_user_ids.include?(user_id) }
58-
.reject { |user_id| users_on_holiday.include?(user_id) }
86+
def assign_user!
87+
return create_post_template if post_template
88+
Assigner
89+
.new(topic, Discourse.system_user)
90+
.assign(assigned_user)
91+
.then do |result|
92+
next if result[:success]
93+
no_one!
94+
end
95+
end
5996

60-
if group_users_ids.empty?
61-
RandomAssignUtils.no_one!(topic_id, group.name)
62-
return
63-
end
97+
def create_post_template
98+
post =
99+
PostCreator.new(
100+
Discourse.system_user,
101+
raw: post_template,
102+
skip_validations: true,
103+
topic_id: topic.id,
104+
).create!
105+
Assigner
106+
.new(post, Discourse.system_user)
107+
.assign(assigned_user)
108+
.then do |result|
109+
next if result[:success]
110+
PostDestroyer.new(Discourse.system_user, post).destroy
111+
no_one!
112+
end
113+
end
64114

65-
max_recently_assigned_days =
66-
(fields.dig("max_recently_assigned_days", "value").presence || 180).to_i.days.ago
67-
last_assignees_ids =
68-
RandomAssignUtils.recently_assigned_users_ids(topic_id, max_recently_assigned_days)
69-
users_ids = group_users_ids - last_assignees_ids
70-
if users_ids.blank?
71-
min_recently_assigned_days =
72-
(fields.dig("min_recently_assigned_days", "value").presence || 14).to_i.days.ago
73-
recently_assigned_users_ids =
74-
RandomAssignUtils.recently_assigned_users_ids(topic_id, min_recently_assigned_days)
75-
users_ids = group_users_ids - recently_assigned_users_ids
76-
end
115+
def group_users
116+
users =
117+
group
118+
.users
119+
.where(id: User.assign_allowed.select(:id))
120+
.where.not(
121+
id:
122+
User
123+
.joins(:_custom_fields)
124+
.where(user_custom_fields: { name: "on_holiday", value: "t" })
125+
.select(:id),
126+
)
127+
return users unless skip_new_users_for_days
128+
users.where("users.created_at < ?", skip_new_users_for_days)
129+
end
77130

78-
if users_ids.blank?
79-
RandomAssignUtils.no_one!(topic_id, group.name)
80-
return
81-
end
131+
def raise_error(message)
132+
raise("[discourse-automation id=#{automation.id}] #{message}.")
133+
end
82134

83-
if fields.dig("in_working_hours", "value")
84-
assign_to_user_id =
85-
users_ids.shuffle.find { |user_id| RandomAssignUtils.in_working_hours?(user_id) }
86-
end
135+
def log_info(message)
136+
Rails.logger.info("[discourse-automation id=#{automation.id}] #{message}.")
137+
end
87138

88-
assign_to_user_id ||= users_ids.sample
89-
if assign_to_user_id.blank?
90-
RandomAssignUtils.no_one!(topic_id, group.name)
91-
return
92-
end
139+
def no_one!
140+
PostCreator.create!(
141+
Discourse.system_user,
142+
topic_id: topic.id,
143+
raw: I18n.t("discourse_automation.scriptables.random_assign.no_one", group: group.name),
144+
validate: false,
145+
)
146+
end
93147

94-
assign_to = User.find(assign_to_user_id)
95-
result = nil
96-
if raw = fields.dig("post_template", "value").presence
97-
post =
98-
PostCreator.new(
99-
Discourse.system_user,
100-
raw: raw,
101-
skip_validations: true,
102-
topic_id: topic.id,
103-
).create!
104-
105-
result = Assigner.new(post, Discourse.system_user).assign(assign_to)
106-
107-
PostDestroyer.new(Discourse.system_user, post).destroy if !result[:success]
108-
else
109-
result = Assigner.new(topic, Discourse.system_user).assign(assign_to)
110-
end
148+
def assigned_recently?
149+
return unless min_hours
150+
TopicCustomField
151+
.where(name: "assigned_to_id", topic: topic)
152+
.where("created_at < ?", min_hours)
153+
.exists?
154+
end
111155

112-
RandomAssignUtils.no_one!(topic_id, group.name) if !result[:success]
156+
def skip_new_users_for_days
157+
days = fields.dig("skip_new_users_for_days", "value").presence
158+
return unless days
159+
days.to_i.days.ago
113160
end
114161

115-
def self.recently_assigned_users_ids(topic_id, from)
116-
posts =
117-
Post
118-
.joins(:user)
119-
.where(topic_id: topic_id, action_code: %w[assigned reassigned assigned_to_post])
120-
.where("posts.created_at > ?", from)
121-
.order(created_at: :desc)
122-
usernames =
123-
Post.custom_fields_for_ids(posts, [:action_code_who]).map { |_, v| v["action_code_who"] }.uniq
124-
User.where(username: usernames).limit(100).pluck(:id)
162+
def max_recently_assigned_days
163+
@max_days ||= (fields.dig("max_recently_assigned_days", "value").presence || 180).to_i.days.ago
125164
end
126165

127-
def self.user_tzinfo(user_id)
166+
def min_recently_assigned_days
167+
@min_days ||= (fields.dig("min_recently_assigned_days", "value").presence || 14).to_i.days.ago
168+
end
169+
170+
def post_template
171+
@post_template ||= fields.dig("post_template", "value").presence
172+
end
173+
174+
def min_hours
175+
hours = fields.dig("minimum_time_between_assignments", "value").presence
176+
return unless hours
177+
hours.to_i.hours.ago
178+
end
179+
180+
def in_working_hours?(user_id)
181+
tzinfo = user_tzinfo(user_id)
182+
tztime = tzinfo.now
183+
184+
!tztime.saturday? && !tztime.sunday? && tztime.hour > 7 && tztime.hour < 11
185+
end
186+
187+
def user_tzinfo(user_id)
128188
timezone = UserOption.where(user_id: user_id).pluck(:timezone).first || "UTC"
129189

130190
tzinfo = nil
@@ -140,20 +200,4 @@ def self.user_tzinfo(user_id)
140200

141201
tzinfo
142202
end
143-
144-
def self.no_one!(topic_id, group)
145-
PostCreator.create!(
146-
Discourse.system_user,
147-
topic_id: topic_id,
148-
raw: I18n.t("discourse_automation.scriptables.random_assign.no_one", group: group),
149-
validate: false,
150-
)
151-
end
152-
153-
def self.in_working_hours?(user_id)
154-
tzinfo = RandomAssignUtils.user_tzinfo(user_id)
155-
tztime = tzinfo.now
156-
157-
!tztime.saturday? && !tztime.sunday? && tztime.hour > 7 && tztime.hour < 11
158-
end
159203
end

0 commit comments

Comments
 (0)