From f657239a4b8a61650f7ee999c0d2c8103c987116 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 14 Mar 2022 10:55:50 +0100 Subject: [PATCH] FEATURE: improves random assign to assign to a post (#300) --- config/locales/client.en.yml | 3 + lib/random_assign_utils.rb | 108 +++++++++++++++++++++ plugin.rb | 75 +------------- spec/lib/random_assign_utils_spec.rb | 140 +++++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 71 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d998ffb..413de3d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -108,6 +108,9 @@ en: scriptables: random_assign: 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: label: Assignees Group minimum_time_between_assignments: diff --git a/lib/random_assign_utils.rb b/lib/random_assign_utils.rb index a9cca52..5bb54b8 100644 --- a/lib/random_assign_utils.rb +++ b/lib/random_assign_utils.rb @@ -1,6 +1,114 @@ # frozen_string_literal: true 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) posts = Post .joins(:user) diff --git a/plugin.rb b/plugin.rb index 67574f8..c1bac32 100644 --- a/plugin.rb +++ b/plugin.rb @@ -857,87 +857,20 @@ after_initialize do require 'random_assign_utils' add_automation_scriptable('random_assign') do - field :assignees_group, component: :group - field :assigned_topic, component: :text + 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 :in_working_hours, component: :boolean + field :post_template, component: :post version 1 triggerables %i[point_in_time recurring] script do |context, fields, automation| - unless SiteSetting.assign_enabled? - 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) + RandomAssignUtils.automation_script!(context, fields, automation) end end end diff --git a/spec/lib/random_assign_utils_spec.rb b/spec/lib/random_assign_utils_spec.rb index 129d7ed..bbb4442 100644 --- a/spec/lib/random_assign_utils_spec.rb +++ b/spec/lib/random_assign_utils_spec.rb @@ -7,9 +7,149 @@ require 'random_assign_utils' describe RandomAssignUtils do before do SiteSetting.assign_enabled = true + + @orig_logger = Rails.logger + Rails.logger = @fake_logger = FakeLogger.new end + after do + Rails.logger = @orig_logger + end + + FakeAutomation = Struct.new(:id) + 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 context 'no one has been assigned' do