FEATURE: improves random assign to assign to a post (#300)

This commit is contained in:
Joffrey JAFFEUX 2022-03-14 10:55:50 +01:00 committed by GitHub
parent 8c382b02c1
commit f657239a4b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 255 additions and 71 deletions

View File

@ -108,6 +108,9 @@ en:
scriptables: scriptables:
random_assign: random_assign:
fields: fields:
post_template:
label: Post template
description: If filled, a post with this template will be created and a user assigned to it instead of the topic.
assignees_group: assignees_group:
label: Assignees Group label: Assignees Group
minimum_time_between_assignments: minimum_time_between_assignments:

View File

@ -1,6 +1,114 @@
# frozen_string_literal: true # frozen_string_literal: true
class RandomAssignUtils class RandomAssignUtils
def self.raise_error(automation, message)
raise("[discourse-automation id=#{automation.id}] #{message}.")
end
def self.log_info(automation, message)
Rails.logger.info("[discourse-automation id=#{automation.id}] #{message}.")
end
def self.automation_script!(context, fields, automation)
unless SiteSetting.assign_enabled?
raise_error(automation, "discourse-assign is not enabled")
end
unless topic_id = fields.dig('assigned_topic', 'value')
raise_error(automation, "`assigned_topic` not provided")
end
unless topic = Topic.find_by(id: topic_id)
raise_error(automation, "Topic(#{topic_id}) not found")
end
min_hours = fields.dig('minimum_time_between_assignments', 'value').presence
if min_hours && TopicCustomField
.where(name: 'assigned_to_id', topic_id: topic_id)
.where('created_at < ?', min_hours.to_i.hours.ago)
.exists?
log_info(automation, "Topic(#{topic_id}) has already been assigned recently")
return
end
unless group_id = fields.dig('assignees_group', 'value')
raise_error(automation, "`assignees_group` not provided")
end
unless group = Group.find_by(id: group_id)
raise_error(automation, "Group(#{group_id}) not found")
end
users_on_holiday = Set.new(
User
.where(id:
UserCustomField
.where(name: 'on_holiday', value: 't')
.select(:user_id)
).pluck(:id)
)
group_users_ids = group
.group_users
.joins(:user)
.pluck('users.id')
.reject { |user_id| users_on_holiday.include?(user_id) }
if group_users_ids.empty?
RandomAssignUtils.no_one!(topic_id, group.name)
return
end
max_recently_assigned_days = (fields.dig('max_recently_assigned_days', 'value').presence || 180).to_i.days.ago
last_assignees_ids = RandomAssignUtils.recently_assigned_users_ids(topic_id, max_recently_assigned_days)
users_ids = group_users_ids - last_assignees_ids
if users_ids.blank?
min_recently_assigned_days = (fields.dig('min_recently_assigned_days', 'value').presence || 14).to_i.days.ago
recently_assigned_users_ids = RandomAssignUtils.recently_assigned_users_ids(topic_id, min_recently_assigned_days)
users_ids = group_users_ids - recently_assigned_users_ids
end
if users_ids.blank?
RandomAssignUtils.no_one!(topic_id, group.name)
return
end
if fields.dig('in_working_hours', 'value')
assign_to_user_id = users_ids.shuffle.find do |user_id|
RandomAssignUtils.in_working_hours?(user_id)
end
end
assign_to_user_id ||= users_ids.sample
if assign_to_user_id.blank?
RandomAssignUtils.no_one!(topic_id, group.name)
return
end
assign_to = User.find(assign_to_user_id)
result = nil
if raw = fields.dig('post_template', 'value').presence
post = PostCreator.new(
Discourse.system_user,
raw: raw,
skip_validations: true,
topic_id: topic.id
).create!
result = Assigner.new(post, Discourse.system_user).assign(assign_to)
if !result[:success]
PostDestroyer.new(Discourse.system_user, post).destroy
end
else
result = Assigner.new(topic, Discourse.system_user).assign(assign_to)
end
if !result[:success]
RandomAssignUtils.no_one!(topic_id, group.name)
end
end
def self.recently_assigned_users_ids(topic_id, from) def self.recently_assigned_users_ids(topic_id, from)
posts = Post posts = Post
.joins(:user) .joins(:user)

View File

@ -857,87 +857,20 @@ after_initialize do
require 'random_assign_utils' require 'random_assign_utils'
add_automation_scriptable('random_assign') do add_automation_scriptable('random_assign') do
field :assignees_group, component: :group field :assignees_group, component: :group, required: true
field :assigned_topic, component: :text field :assigned_topic, component: :text, required: true
field :minimum_time_between_assignments, component: :text field :minimum_time_between_assignments, component: :text
field :max_recently_assigned_days, component: :text field :max_recently_assigned_days, component: :text
field :min_recently_assigned_days, component: :text field :min_recently_assigned_days, component: :text
field :in_working_hours, component: :boolean field :in_working_hours, component: :boolean
field :post_template, component: :post
version 1 version 1
triggerables %i[point_in_time recurring] triggerables %i[point_in_time recurring]
script do |context, fields, automation| script do |context, fields, automation|
unless SiteSetting.assign_enabled? RandomAssignUtils.automation_script!(context, fields, automation)
Rails.logger.warn("[discourse-automation id=#{automation.id}] discourse-assign is not enabled.")
next
end
next unless topic_id = fields.dig('assigned_topic', 'value')
next unless topic = Topic.find_by(id: topic_id)
next unless group_id = fields.dig('assignees_group', 'value')
next unless group = Group.find_by(id: group_id)
min_hours = fields.dig('minimum_time_between_assignments', 'value').presence
if min_hours && TopicCustomField
.where(name: 'assigned_to_id', topic_id: topic_id)
.where('created_at < ?', min_hours.to_i.hours.ago)
.exists?
next
end
users_on_holiday = Set.new(
User
.where(id:
UserCustomField
.where(name: 'on_holiday', value: 't')
.pluck(:user_id)
).pluck(:id)
)
group_users_ids = group
.group_users
.joins(:user)
.pluck('users.id')
.reject { |user_id| users_on_holiday.include?(user_id) }
if group_users_ids.empty?
RandomAssignUtils.no_one!(topic_id, group.name)
next
end
last_assignees_ids = RandomAssignUtils.recently_assigned_users_ids(
topic_id,
(fields.dig('max_recently_assigned_days', 'value').presence || 180).to_i.days.ago
)
users_ids = group_users_ids - last_assignees_ids
if users_ids.blank?
recently_assigned_users_ids = RandomAssignUtils.recently_assigned_users_ids(topic_id, (fields.dig('min_recently_assigned_days', 'value').presence || 14).to_i.days.ago)
users_ids = group_users_ids - recently_assigned_users_ids
end
if users_ids.blank?
RandomAssignUtils.no_one!(topic_id, group.name)
next
end
if fields.dig('in_working_hours', 'value')
assign_to_user_id = users_ids.shuffle.find do |user_id|
RandomAssignUtils.in_working_hours?(user_id)
end
end
assign_to_user_id ||= users_ids.sample
if assign_to_user_id.blank?
RandomAssignUtils.no_one!(topic_id, group.name)
next
end
assign_to = User.find_by(id: assign_to_user_id)
assign_to && Assigner.new(topic, Discourse.system_user).assign(assign_to)
end end
end end
end end

View File

@ -7,9 +7,149 @@ require 'random_assign_utils'
describe RandomAssignUtils do describe RandomAssignUtils do
before do before do
SiteSetting.assign_enabled = true SiteSetting.assign_enabled = true
@orig_logger = Rails.logger
Rails.logger = @fake_logger = FakeLogger.new
end end
after do
Rails.logger = @orig_logger
end
FakeAutomation = Struct.new(:id)
let(:post) { Fabricate(:post) } let(:post) { Fabricate(:post) }
let!(:automation) { FakeAutomation.new(1) }
describe '.automation_script!' do
context 'all users of group are on holidays' do
fab!(:topic_1) { Fabricate(:topic) }
fab!(:group_1) { Fabricate(:group) }
fab!(:user_1) { Fabricate(:user) }
before do
group_1.add(user_1)
UserCustomField.create!(name: 'on_holiday', value: 't', user_id: user_1.id)
end
it 'creates post on the topic' do
described_class.automation_script!({}, { 'assignees_group' => { 'value' => group_1.id }, 'assigned_topic' => { 'value' => topic_1.id } }, automation)
expect(topic_1.posts.first.raw).to match("Attempted randomly assign a member of @#{group_1.name}, but no one was available.")
end
end
context 'all users of group have been assigned recently' do
fab!(:topic_1) { Fabricate(:topic) }
fab!(:group_1) { Fabricate(:group) }
fab!(:user_1) { Fabricate(:user) }
before do
Assigner.new(topic_1, Discourse.system_user).assign(user_1)
group_1.add(user_1)
end
it 'creates post on the topic' do
described_class.automation_script!({}, { 'assignees_group' => { 'value' => group_1.id }, 'assigned_topic' => { 'value' => topic_1.id } }, automation)
expect(topic_1.posts.first.raw).to match("Attempted randomly assign a member of @#{group_1.name}, but no one was available.")
end
end
context 'user can be assigned' do
fab!(:group_1) { Fabricate(:group) }
fab!(:user_1) { Fabricate(:user) }
fab!(:topic_1) { Fabricate(:topic) }
before do
SiteSetting.assign_allowed_on_groups = [group_1.id.to_s].join('|')
group_1.add(user_1)
end
context 'post_template is set' do
it 'creates a post with the template and assign the user' do
described_class.automation_script!({}, { 'post_template' => { 'value' => 'this is a post template' }, 'assignees_group' => { 'value' => group_1.id }, 'assigned_topic' => { 'value' => topic_1.id } }, automation)
expect(topic_1.posts.first.raw).to match('this is a post template')
end
end
context 'post_template is not set' do
fab!(:post_1) { Fabricate(:post, topic: topic_1) }
it 'assigns the user to the topic' do
described_class.automation_script!({}, { 'assignees_group' => { 'value' => group_1.id }, 'assigned_topic' => { 'value' => topic_1.id } }, automation)
expect(topic_1.assignment.assigned_to_id).to eq(user_1.id)
end
end
end
context 'all users in working hours' do
fab!(:topic_1) { Fabricate(:topic) }
fab!(:group_1) { Fabricate(:group) }
fab!(:user_1) { Fabricate(:user) }
before do
freeze_time('2022-10-01 02:00')
UserOption.find_by(user_id: user_1.id).update(timezone: 'Europe/Paris')
group_1.add(user_1)
end
it 'creates post on the topic' do
described_class.automation_script!({}, { 'in_working_hours' => { 'value' => true }, 'assignees_group' => { 'value' => group_1.id }, 'assigned_topic' => { 'value' => topic_1.id } }, automation)
expect(topic_1.posts.first.raw).to match("Attempted randomly assign a member of @#{group_1.name}, but no one was available.")
end
end
context 'assignees_group not provided' do
fab!(:topic_1) { Fabricate(:topic) }
it 'raises an error' do
expect {
described_class.automation_script!({}, { 'assigned_topic' => { 'value' => topic_1.id } }, automation)
}.to raise_error(/`assignees_group` not provided/)
end
end
context 'assignees_group not found' do
fab!(:topic_1) { Fabricate(:topic) }
it 'raises an error' do
expect {
described_class.automation_script!({}, { 'assigned_topic' => { 'value' => topic_1.id }, 'assignees_group' => { 'value' => -1 } }, automation)
}.to raise_error(/Group\(-1\) not found/)
end
end
context 'assigned_topic not provided' do
it 'raises an error' do
expect {
described_class.automation_script!({}, {}, automation)
}.to raise_error(/`assigned_topic` not provided/)
end
end
context 'assigned_topic is not found' do
it 'raises an error' do
expect {
described_class.automation_script!({}, { 'assigned_topic' => { 'value' => 1 } }, automation)
}.to raise_error(/Topic\(1\) not found/)
end
end
context 'minimum_time_between_assignments is set' do
context 'the topic has been assigned recently' do
fab!(:topic_1) { Fabricate(:topic) }
before do
freeze_time
TopicCustomField.create!(name: 'assigned_to_id', topic_id: topic_1.id, created_at: 20.hours.ago)
end
it 'logs a warning' do
described_class.automation_script!({}, { 'assigned_topic' => { 'value' => topic_1.id }, 'minimum_time_between_assignments' => { 'value' => 10 } }, automation)
expect(Rails.logger.infos.first).to match(/Topic\(#{topic_1.id}\) has already been assigned recently/)
end
end
end
end
describe '.recently_assigned_users_ids' do describe '.recently_assigned_users_ids' do
context 'no one has been assigned' do context 'no one has been assigned' do