FEATURE: Assignment target is polymorphic (#218)

FEATURE: Assignment target is polymorphic (#218)

Change topic_id to polymorphic approach (In the next step we will allow assigning to individual post)

topic_id column is still used for efficient display of assigned users on topic list (to avoid scanning posts)

diff --git a/app/controllers/discourse_assign/assign_controller.rb b/app/controllers/discourse_assign/assign_controller.rb
index 22c54e9..c41cf55 100644
--- a/app/controllers/discourse_assign/assign_controller.rb
+++ b/app/controllers/discourse_assign/assign_controller.rb
@@ -31,27 +31,34 @@ module DiscourseAssign
     end
 
     def unassign
-      topic_id = params.require(:topic_id)
-      topic = Topic.find(topic_id.to_i)
-      assigner = TopicAssigner.new(topic, current_user)
+      target_id = params.require(:target_id)
+      target_type = params.require(:target_type)
+      raise Discourse::NotFound if !Assignment.valid_type?(target_type)
+      target = target_type.constantize.where(id: target_id).first
+      raise Discourse::NotFound unless target
+
+      assigner = Assigner.new(target, current_user)
       assigner.unassign
 
       render json: success_json
     end
 
     def assign
-      topic_id = params.require(:topic_id)
+      target_id = params.require(:target_id)
+      target_type = params.require(:target_type)
       username = params.permit(:username)['username']
       group_name = params.permit(:group_name)['group_name']
 
-      topic = Topic.find(topic_id.to_i)
       assign_to = username.present? ? User.find_by(username_lower: username.downcase) : Group.where("LOWER(name) = ?", group_name.downcase).first
 
       raise Discourse::NotFound unless assign_to
+      raise Discourse::NotFound if !Assignment.valid_type?(target_type)
+      target = target_type.constantize.where(id: target_id).first
+      raise Discourse::NotFound unless target
 
       # perhaps?
       #Scheduler::Defer.later "assign topic" do
-      assign = TopicAssigner.new(topic, current_user).assign(assign_to)
+      assign = Assigner.new(target, current_user).assign(assign_to)
 
       if assign[:success]
         render json: success_json
@@ -69,17 +76,17 @@ module DiscourseAssign
       topics = Topic
         .includes(:tags)
         .includes(:user)
-        .joins("JOIN assignments a ON a.topic_id = topics.id AND a.assigned_to_id IS NOT NULL")
+        .joins("JOIN assignments a ON a.target_id = topics.id AND a.target_type = 'Topic' AND a.assigned_to_id IS NOT NULL")
         .order("a.assigned_to_id, topics.bumped_at desc")
         .offset(offset)
         .limit(limit)
 
       Topic.preload_custom_fields(topics, TopicList.preloaded_custom_fields)
 
-      assignments = Assignment.where(topic: topics).pluck(:topic_id, :assigned_to_id).to_h
+      topic_assignments = Assignment.where(target_id: topics.map(&:id), target_type: 'Topic').pluck(:target_id, :assigned_to_id).to_h
 
       users = User
-        .where("users.id IN (?)", assignments.values.uniq)
+        .where("users.id IN (?)", topic_assignments.values.uniq)
         .joins("join user_emails on user_emails.user_id = users.id AND user_emails.primary")
         .select(UserLookup.lookup_columns)
         .to_a
@@ -89,7 +96,7 @@ module DiscourseAssign
       users_map = users.index_by(&:id)
 
       topics.each do |topic|
-        user_id = assignments[topic.id]
+        user_id = topic_assignments[topic.id]
         topic.preload_assigned_to(users_map[user_id]) if user_id
       end
 
@@ -111,7 +118,7 @@ module DiscourseAssign
       members = User
         .joins("LEFT OUTER JOIN group_users g ON g.user_id = users.id")
         .joins("LEFT OUTER JOIN assignments a ON a.assigned_to_id = users.id AND a.assigned_to_type = 'User'")
-        .joins("LEFT OUTER JOIN topics t ON t.id = a.topic_id")
+        .joins("LEFT OUTER JOIN topics t ON t.id = a.target_id AND a.target_type = 'Topic'")
         .where("g.group_id = ? AND users.id > 0 AND t.deleted_at IS NULL", group.id)
         .where("a.assigned_to_id IS NOT NULL")
         .order('COUNT(users.id) DESC')
@@ -127,14 +134,14 @@ module DiscourseAssign
       end
 
       group_assignment_count = Topic
-        .joins("JOIN assignments a ON a.topic_id = topics.id")
+        .joins("JOIN assignments a ON a.target_id = topics.id AND a.target_type = 'Topic'")
         .where(<<~SQL, group_id: group.id)
           a.assigned_to_id = :group_id AND a.assigned_to_type = 'Group'
         SQL
         .count
 
       assignment_count = Topic
-        .joins("JOIN assignments a ON a.topic_id = topics.id")
+        .joins("JOIN assignments a ON a.target_id = topics.id AND a.target_type = 'Topic'")
         .joins("JOIN group_users ON group_users.user_id = a.assigned_to_id ")
         .where("group_users.group_id = ?", group.id)
         .where("a.assigned_to_type = 'User'")
diff --git a/app/models/assignment.rb b/app/models/assignment.rb
index 9568402..ac67f17 100644
--- a/app/models/assignment.rb
+++ b/app/models/assignment.rb
@@ -1,9 +1,18 @@
 # frozen_string_literal: true
 
 class Assignment < ActiveRecord::Base
+  VALID_TYPES = %w(topic).freeze
+
   belongs_to :topic
   belongs_to :assigned_to, polymorphic: true
   belongs_to :assigned_by_user, class_name: "User"
+  belongs_to :target, polymorphic: true
+
+  scope :joins_with_topics, -> { joins("INNER JOIN topics ON topics.id = assignments.target_id AND assignments.target_type = 'Topic' AND topics.deleted_at IS NULL") }
+
+  def self.valid_type?(type)
+    VALID_TYPES.include?(type.downcase)
+  end
 
   def assigned_to_user?
     assigned_to_type == 'User'
diff --git a/assets/javascripts/discourse-assign/controllers/assign-user.js.es6 b/assets/javascripts/discourse-assign/controllers/assign-user.js.es6
index cf4df58..f291f94 100644
--- a/assets/javascripts/discourse-assign/controllers/assign-user.js.es6
+++ b/assets/javascripts/discourse-assign/controllers/assign-user.js.es6
@@ -94,7 +94,8 @@ export default Controller.extend({
       data: {
         username: this.get("model.username"),
         group_name: this.get("model.group_name"),
-        topic_id: this.get("model.topic.id"),
+        target_id: this.get("model.topic.id"),
+        target_type: "Topic",
       },
     })
       .then(() => {
diff --git a/assets/javascripts/discourse/services/task-actions.js.es6 b/assets/javascripts/discourse/services/task-actions.js.es6
index f78279b..853865a 100644
--- a/assets/javascripts/discourse/services/task-actions.js.es6
+++ b/assets/javascripts/discourse/services/task-actions.js.es6
@@ -6,7 +6,10 @@ export default Service.extend({
   unassign(topicId) {
     return ajax("/assign/unassign", {
       type: "PUT",
-      data: { topic_id: topicId },
+      data: {
+        target_id: topicId,
+        target_type: "Topic",
+      },
     });
   },
 
@@ -25,7 +28,8 @@ export default Service.extend({
       type: "PUT",
       data: {
         username: user.username,
-        topic_id: topic.id,
+        target_id: topic.id,
+        target_type: "Topic",
       },
     });
   },
diff --git a/db/migrate/20211006223156_add_target_to_assignments.rb b/db/migrate/20211006223156_add_target_to_assignments.rb
new file mode 100644
index 0000000..b6d45d2
--- /dev/null
+++ b/db/migrate/20211006223156_add_target_to_assignments.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddTargetToAssignments < ActiveRecord::Migration[6.1]
+  def up
+    add_column :assignments, :target_id, :integer
+    add_column :assignments, :target_type, :string
+
+    execute <<~SQL
+      UPDATE assignments
+      SET target_type = 'Topic', target_id = topic_id
+      WHERE target_type IS NULL
+    SQL
+
+    change_column :assignments, :target_id, :integer, null: false
+    change_column :assignments, :target_type, :string, null: false
+
+    add_index :assignments, [:target_id, :target_type], unique: true
+    add_index :assignments, [:assigned_to_id, :assigned_to_type, :target_id, :target_type], unique: true, name: 'unique_target_and_assigned'
+  end
+
+  def down
+    remove_columns :assignments, :target_id, :target_type
+  end
+end

[... diff too long, it was truncated ...]

GitHub sha: dc8f43fbb1541b269d049eae47db01c86855b704

This commit appears in #218 which was approved by eviltrout. It was merged by lis2.