FEATURE: improves random assign to assign to a post (#300)
This commit is contained in:
parent
8c382b02c1
commit
f657239a4b
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
75
plugin.rb
75
plugin.rb
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue