diff --git a/app/controllers/discourse_assign/assign_controller.rb b/app/controllers/discourse_assign/assign_controller.rb index b77f055..2753480 100644 --- a/app/controllers/discourse_assign/assign_controller.rb +++ b/app/controllers/discourse_assign/assign_controller.rb @@ -70,5 +70,11 @@ module DiscourseAssign render json: success_json end + + def unassign_all + user = User.find_by(id: params[:user_id]) + TopicAssigner.unassign_all(user, current_user) + render json: success_json + end end end diff --git a/assets/javascripts/discourse-assign/controllers/user-activity-assigned.js.es6 b/assets/javascripts/discourse-assign/controllers/user-activity-assigned.js.es6 new file mode 100644 index 0000000..5545517 --- /dev/null +++ b/assets/javascripts/discourse-assign/controllers/user-activity-assigned.js.es6 @@ -0,0 +1,28 @@ +import { ajax } from 'discourse/lib/ajax'; +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Controller.extend({ + user: Ember.inject.controller(), + + @computed('model.topics') + canUnassignAll(topics) { + return topics && topics.length && this.currentUser.get('staff'); + }, + + actions: { + unassignAll() { + let user = this.get('user.model'); + bootbox.confirm( + I18n.t("discourse_assign.unassign_all.confirm", { username: user.get('username') }), + value => { + if (value) { + ajax("/assign/unassign-all", { + type: 'PUT', + data: { user_id: user.get('id') } + }).then(() => this.send('unassignedAll')); + } + } + ); + } + } +}); diff --git a/assets/javascripts/discourse-assign/routes/user-activity-assigned.js.es6 b/assets/javascripts/discourse-assign/routes/user-activity-assigned.js.es6 index f59c63c..aedf3d2 100644 --- a/assets/javascripts/discourse-assign/routes/user-activity-assigned.js.es6 +++ b/assets/javascripts/discourse-assign/routes/user-activity-assigned.js.es6 @@ -3,7 +3,30 @@ import UserTopicListRoute from "discourse/routes/user-topic-list"; export default UserTopicListRoute.extend({ userActionType: 16, noContentHelpKey: "discourse_assigns.no_assigns", - model: function() { - return this.store.findFiltered('topicList', {filter: 'latest', params: {assigned: this.modelFor('user').get('username_lower') }}); + + model() { + return this.store.findFiltered( + 'topicList', + { + filter: 'latest', + params: { assigned: this.modelFor('user').get('username_lower') } + } + ); + }, + + renderTemplate() { + this.render('user-activity-assigned'); + this.render('user-topics-list', { into: 'user-activity-assigned' }); + }, + + setupController(controller, model) { + this._super(controller, model); + controller.set('model', model); + }, + + actions: { + unassignedAll() { + this.refresh(); + } } }); diff --git a/assets/javascripts/discourse/templates/user-activity-assigned.hbs b/assets/javascripts/discourse/templates/user-activity-assigned.hbs new file mode 100644 index 0000000..c67894b --- /dev/null +++ b/assets/javascripts/discourse/templates/user-activity-assigned.hbs @@ -0,0 +1,11 @@ +{{#if canUnassignAll}} +
{{username}} {{description}}
" + unassign_all: + title: "Unassign All Topics" + confirm: "Are you sure you want to unassign all topics from {{username}}?" + unassign: title: "Unassign" help: "Unassign Topic" diff --git a/config/routes.rb b/config/routes.rb index e5404df..03a1f80 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,5 +2,6 @@ DiscourseAssign::Engine.routes.draw do put "/claim/:topic_id" => "assign#claim" put "/assign" => "assign#assign" put "/unassign" => "assign#unassign" + put "/unassign-all" => "assign#unassign_all" get "/suggestions" => "assign#suggestions" end diff --git a/jobs/unassign_bulk.rb b/jobs/unassign_bulk.rb new file mode 100644 index 0000000..06cb82a --- /dev/null +++ b/jobs/unassign_bulk.rb @@ -0,0 +1,10 @@ +module Jobs + class UnassignBulk < Jobs::Base + def execute(args) + assigned_by = User.find(args[:assigned_by_id]) + Topic.where(id: args[:topic_ids]).each do |t| + TopicAssigner.new(t, assigned_by).unassign + end + end + end +end diff --git a/lib/topic_assigner.rb b/lib/topic_assigner.rb index 4813532..53a4bcc 100644 --- a/lib/topic_assigner.rb +++ b/lib/topic_assigner.rb @@ -1,6 +1,25 @@ require_dependency 'email/sender' class ::TopicAssigner + + def self.unassign_all(user, assigned_by) + topic_ids = TopicCustomField.where(name: 'assigned_to_id', value: user.id).pluck(:topic_id) + + # Fast path: by doing this we can instantly refresh for the user showing no assigned topics + # while doing the "full" removal asynchronously. + TopicCustomField.where( + name: ['assigned_to_id', 'assigned_by_id'], + topic_id: topic_ids + ).delete_all + + Jobs.enqueue( + :unassign_bulk, + user_id: user.id, + assigned_by_id: assigned_by.id, + topic_ids: topic_ids + ) + end + def self.backfill_auto_assign staff_mention = User.where('moderator OR admin') .pluck('username') @@ -95,9 +114,7 @@ SQL end def assign(assign_to, silent: false) - @topic.custom_fields["assigned_to_id"] = assign_to.id - @topic.custom_fields["assigned_by_id"] = @assigned_by.id - @topic.save! + @topic.upsert_custom_fields(assigned_to_id: assign_to.id, assigned_by_id: @assigned_by.id) first_post = @topic.posts.find_by(post_number: 1) first_post.publish_change_to_clients!(:revised, reload_topic: true) @@ -162,9 +179,7 @@ SQL def unassign(silent: false) if assigned_to_id = @topic.custom_fields["assigned_to_id"] - @topic.custom_fields["assigned_to_id"] = nil - @topic.custom_fields["assigned_by_id"] = nil - @topic.save! + @topic.upsert_custom_fields(assigned_to_id: nil, assigned_by_id: nil) post = @topic.posts.where(post_number: 1).first return unless post.present? diff --git a/plugin.rb b/plugin.rb index b55fc80..1b8be3e 100644 --- a/plugin.rb +++ b/plugin.rb @@ -15,6 +15,7 @@ Discourse::Application.routes.append do end after_initialize do + require File.expand_path('../jobs/unassign_bulk', __FILE__) require 'topic_assigner' # Raise an invalid access error if a user tries to act on something diff --git a/spec/lib/topic_assigner_spec.rb b/spec/lib/topic_assigner_spec.rb index 7e13d3f..bac7f53 100644 --- a/spec/lib/topic_assigner_spec.rb +++ b/spec/lib/topic_assigner_spec.rb @@ -22,4 +22,25 @@ RSpec.describe TopicAssigner do assert_publish_topic_state(pm, user) { assigner.unassign } end end + + context "assigning and unassigning" do + let(:post) { Fabricate(:post) } + let(:topic) { post.topic } + let(:moderator) { Fabricate(:moderator) } + let(:assigner) { TopicAssigner.new(topic, moderator) } + + it "can assign and unassign correctly" do + assigner.assign(moderator) + expect(TopicQuery.new(moderator, assigned: moderator.username).list_latest.topics).to be_present + assigner.unassign + expect(TopicQuery.new(moderator, assigned: moderator.username).list_latest.topics).to be_blank + end + + it "can unassign all a user's topics at once" do + assigner.assign(moderator) + TopicAssigner.unassign_all(moderator, moderator) + expect(TopicQuery.new(moderator, assigned: moderator.username).list_latest.topics).to be_blank + end + + end end