FEATURE: improves random assign automation (#166)

FEATURE: improves random assign automation (#166)

  • Makes a best attempt at being random and assigning people who haven’t been assigned for a long time
  • Uses timezones and holidays
  • Allows to define a minimum delay between assignments
  • Creates a post if no one is available
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 260bab0..0db7639 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -75,5 +75,7 @@ en:
           fields:
             assignees_group:
               label: Assignees Group
+            minimum_time_between_assignments:
+              label: Hours between assignments
             assigned_topic:
               label: Assigned Topic ID
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 603d0c1..c19ebb6 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -66,3 +66,4 @@ en:
     scriptables:
       random_assign:
         title: Random assign
+        no_one: "Attempted randomly assign a member of @%{group}, but no one was available."
diff --git a/lib/random_assign_utils.rb b/lib/random_assign_utils.rb
new file mode 100644
index 0000000..0360e9d
--- /dev/null
+++ b/lib/random_assign_utils.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class RandomAssignUtils
+  def self.user_tzinfo(user_id)
+    timezone = UserOption.where(user_id: user_id).pluck(:timezone).first || "UTC"
+
+    tzinfo = nil
+    begin
+      tzinfo = ActiveSupport::TimeZone.find_tzinfo(timezone)
+    rescue TZInfo::InvalidTimezoneIdentifier
+      Rails.logger.warn("#{User.find_by(id: user_id)&.username} has the timezone #{timezone} set, we do not know how to parse it in Rails (assuming UTC)")
+      timezone = "UTC"
+      tzinfo = ActiveSupport::TimeZone.find_tzinfo(timezone)
+    end
+
+    tzinfo
+  end
+
+  def self.no_one!(topic_id, group)
+    PostCreator.create!(
+      Discourse.system_user,
+      topic_id: topic_id,
+      raw: I18n.t("discourse_automation.scriptables.random_assign.no_one", group: group),
+      validate: false
+    )
+  end
+
+  def self.in_working_hours?(user_id)
+    tzinfo = RandomAssignUtils.user_tzinfo(user_id)
+    tztime = tzinfo.now
+
+    !tztime.saturday? &&
+    !tztime.sunday? &&
+    tztime.hour > 7 &&
+    tztime.hour < 11
+  end
+end
diff --git a/plugin.rb b/plugin.rb
index bb6fa4e..8a6d27c 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -539,9 +539,12 @@ after_initialize do
   end
 
   if defined?(DiscourseAutomation)
+    require 'random_assign_utils'
+
     add_automation_scriptable('random_assign') do
       field :assignees_group, component: :group
       field :assigned_topic, component: :text
+      field :minimum_time_between_assignments, component: :text
 
       version 1
 
@@ -550,14 +553,76 @@ after_initialize do
       script do |context, fields|
         next unless SiteSetting.assign_enabled?
 
+        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)
-        assign_to = group.group_users.order(Arel.sql('RANDOM()')).first.user
 
-        next unless topic_id = fields.dig('assigned_topic', 'value')
-        next unless topic = Topic.find_by(id: topic_id)
+        min_hours = (fields.dig('minimum_time_between_assignments', 'value') || 12).to_i
+        if TopicCustomField
+            .where(name: 'assigned_to_id', topic_id: topic_id)
+            .where('created_at < ?', min_hours.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 = UserAction
+          .joins(:user)
+          .where(action_type: UserAction::ASSIGNED, target_topic_id: topic_id)
+          .where('user_actions.created_at > ?', 6.months.ago)
+          .order(created_at: :desc)
+          .limit(group_users_ids.length)
+          .pluck('users.id')
+          .uniq
+
+        users_ids = group_users_ids - last_assignees_ids
+        if users_ids.blank?
+          recently_assigned_users_ids = UserAction
+            .joins(:user)
+            .where(action_type: UserAction::ASSIGNED, target_topic_id: topic_id)
+            .where('user_actions.created_at < ?', 2.weeks.ago)
+            .pluck('users.id')
+            .uniq
+          users_ids = group_users_ids - recently_assigned_users_ids
+        end
+
+        if users_ids.blank?
+          RandomAssignUtils.no_one!(topic_id, group.name)
+          next
+        end
+
+        assign_to_user_id = users_ids.shuffle.find do |user_id|
+          RandomAssignUtils.in_working_hours?(user_id)
+        end
+
+        if assign_to_user_id.blank?
+          RandomAssignUtils.no_one!(topic_id, group.name)
+          next
+        end
 
-        TopicAssigner.new(topic, Discourse.system_user).assign(assign_to)
+        assign_to = User.find_by(id: assign_to_user_id)
+        assign_to && TopicAssigner.new(topic, Discourse.system_user).assign(assign_to)
       end
     end
   end

GitHub sha: 0046041e9e20b8d9e17bb7539280bc65b873a321

This commit appears in #166 which was approved by eviltrout. It was merged by jjaffeux.