diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..7b8ffc0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,101 @@ +{ + "env": { + "jasmine": true, + "node": true, + "mocha": true, + "browser": true, + "builtin": true + }, + "parserOptions": { + "ecmaVersion": 7, + "sourceType": "module" + }, + "globals": + {"Ember":true, + "jQuery":true, + "$":true, + "QUnit":true, + "RSVP":true, + "Discourse":true, + "Em":true, + "Handlebars":true, + "I18n":true, + "bootbox":true, + "moduleFor":true, + "moduleForComponent":true, + "Pretender":true, + "sandbox":true, + "controllerFor":true, + "test":true, + "visit":true, + "andThen":true, + "click":true, + "currentPath":true, + "currentRouteName":true, + "currentURL":true, + "fillIn":true, + "keyEvent":true, + "triggerEvent":true, + "count":true, + "exists":true, + "visible":true, + "invisible":true, + "asyncRender":true, + "selectDropdown":true, + "selectBox":true, + "asyncTestDiscourse":true, + "fixture":true, + "find":true, + "sinon":true, + "moment":true, + "_":true, + "alert":true, + "define":true, + "require":true, + "requirejs":true, + "hasModule":true, + "Blob":true, + "File":true}, + "rules": { + "block-scoped-var": 2, + "dot-notation": 0, + "eqeqeq": [ + 2, + "allow-null" + ], + "guard-for-in": 2, + "no-bitwise": 2, + "no-caller": 2, + "no-cond-assign": 0, + "no-debugger": 2, + "no-empty": 0, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-parens": 0, + "no-inner-declarations": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-new": 2, + "no-plusplus": 0, + "no-proto": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow": 2, + "no-undef": 2, + "no-unused-vars": 2, + "no-with": 2, + "no-this-before-super": 2, + "semi": 2, + "strict": 0, + "valid-typeof": 2, + "wrap-iife": [ + 2, + "inside" + ], + "no-mixed-spaces-and-tabs": 2, + "no-trailing-spaces": 2 + }, + "parser": "babel-eslint" +} diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..a529980 --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,112 @@ +AllCops: + TargetRubyVersion: 2.4 + DisabledByDefault: true + Exclude: + - 'db/schema.rb' + - 'bundle/**/*' + - 'vendor/**/*' + - 'node_modules/**/*' + +# Prefer &&/|| over and/or. +Style/AndOr: + Enabled: true + +# Do not use braces for hash literals when they are the last argument of a +# method call. +Style/BracesAroundHashParameters: + Enabled: true + +# Align `when` with `case`. +Layout/CaseIndentation: + Enabled: true + +# Align comments with method definitions. +Layout/CommentIndentation: + Enabled: true + +# No extra empty lines. +Layout/EmptyLines: + Enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + Enabled: true + +# Two spaces, no tabs (for indentation). +Layout/IndentationWidth: + Enabled: true + +Layout/SpaceAfterColon: + Enabled: true + +Layout/SpaceAfterComma: + Enabled: true + +Layout/SpaceAroundEqualsInParameterDefault: + Enabled: true + +Layout/SpaceAroundKeyword: + Enabled: true + +Layout/SpaceAroundOperators: + Enabled: true + +Layout/SpaceBeforeFirstArg: + Enabled: true + +# Defining a method with parameters needs parentheses. +Style/MethodDefParentheses: + Enabled: true + +# Use `foo {}` not `foo{}`. +Layout/SpaceBeforeBlockBraces: + Enabled: true + +# Use `foo { bar }` not `foo {bar}`. +Layout/SpaceInsideBlockBraces: + Enabled: true + +# Use `{ a: 1 }` not `{a:1}`. +Layout/SpaceInsideHashLiteralBraces: + Enabled: true + +Layout/SpaceInsideParens: + Enabled: true + +# Detect hard tabs, no hard tabs. +Layout/Tab: + Enabled: true + +# Blank lines should not have any spaces. +Layout/TrailingBlankLines: + Enabled: true + +# No trailing whitespace. +Layout/TrailingWhitespace: + Enabled: true + +Lint/Debugger: + Enabled: true + +Lint/BlockAlignment: + Enabled: true + +# Align `end` with the matching keyword or starting expression except for +# assignments, where it should be aligned with the LHS. +Lint/EndAlignment: + Enabled: true + EnforcedStyleAlignWith: variable + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + Enabled: true + +Layout/MultilineMethodCallIndentation: + Enabled: true + EnforcedStyle: indented + +Layout/AlignHash: + Enabled: true + +Bundler/OrderedGems: + Enabled: false diff --git a/app/controllers/discourse_assign/assign_controller.rb b/app/controllers/discourse_assign/assign_controller.rb new file mode 100644 index 0000000..b77f055 --- /dev/null +++ b/app/controllers/discourse_assign/assign_controller.rb @@ -0,0 +1,74 @@ +module DiscourseAssign + class AssignController < Admin::AdminController + before_action :ensure_logged_in + + def suggestions + users = [current_user] + users += User + .where('admin OR moderator') + .where('users.id <> ?', current_user.id) + .joins("join ( + SELECT value::integer user_id, MAX(created_at) last_assigned + FROM topic_custom_fields + WHERE name = 'assigned_to_id' + GROUP BY value::integer + ) as X ON X.user_id = users.id") + .order('X.last_assigned DESC') + .limit(6) + + render json: ActiveModel::ArraySerializer.new(users, + scope: guardian, each_serializer: BasicUserSerializer) + end + + def claim + topic_id = params.require(:topic_id).to_i + topic = Topic.find(topic_id) + + assigned = TopicCustomField.where( + "topic_id = :topic_id AND name = 'assigned_to_id' AND value IS NOT NULL", + topic_id: topic_id + ).pluck(:value) + + if assigned && user_id = assigned[0] + extras = nil + if user = User.where(id: user_id).first + extras = { + assigned_to: serialize_data(user, BasicUserSerializer, root: false) + } + end + return render_json_error(I18n.t('discourse_assign.already_claimed'), extras: extras) + end + + assigner = TopicAssigner.new(topic, current_user) + assigner.assign(current_user) + render json: success_json + end + + def unassign + topic_id = params.require(:topic_id) + topic = Topic.find(topic_id.to_i) + assigner = TopicAssigner.new(topic, current_user) + assigner.unassign + + render json: success_json + end + + def assign + topic_id = params.require(:topic_id) + username = params.require(:username) + + topic = Topic.find(topic_id.to_i) + assign_to = User.find_by(username_lower: username.downcase) + + raise Discourse::NotFound unless assign_to + + assigner = TopicAssigner.new(topic, current_user) + + # perhaps? + #Scheduler::Defer.later "assign topic" do + assigner.assign(assign_to) + + render json: success_json + end + end +end diff --git a/assets/javascripts/discourse-assign/connectors/flagged-topic-details-header/assigned-to.hbs b/assets/javascripts/discourse-assign/connectors/flagged-topic-details-header/assigned-to.hbs new file mode 100644 index 0000000..c9c4eb6 --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/flagged-topic-details-header/assigned-to.hbs @@ -0,0 +1,4 @@ +
+ {{flagged-topic-listener topic=topic}} + {{claim-topic topic=topic}} +
diff --git a/assets/javascripts/discourse-assign/connectors/flagged-topic-header-row/add-assigned-header.hbs b/assets/javascripts/discourse-assign/connectors/flagged-topic-header-row/add-assigned-header.hbs new file mode 100644 index 0000000..6473abd --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/flagged-topic-header-row/add-assigned-header.hbs @@ -0,0 +1 @@ +{{i18n "discourse_assign.assigned"}} diff --git a/assets/javascripts/discourse-assign/connectors/flagged-topic-row/add-claim-controls.hbs b/assets/javascripts/discourse-assign/connectors/flagged-topic-row/add-claim-controls.hbs new file mode 100644 index 0000000..849ffd0 --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/flagged-topic-row/add-claim-controls.hbs @@ -0,0 +1,3 @@ + + {{claim-topic topic=topic}} + diff --git a/assets/javascripts/discourse-assign/connectors/flagged-topic-row/add-claim-controls.js.es6 b/assets/javascripts/discourse-assign/connectors/flagged-topic-row/add-claim-controls.js.es6 new file mode 100644 index 0000000..66bf531 --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/flagged-topic-row/add-claim-controls.js.es6 @@ -0,0 +1,7 @@ +export default { + actions: { + claim(topic) { + console.log('claim:', topic); + } + } +} diff --git a/assets/javascripts/discourse-assign/connectors/flagged-topics-before/add-listener.hbs b/assets/javascripts/discourse-assign/connectors/flagged-topics-before/add-listener.hbs new file mode 100644 index 0000000..6eee759 --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/flagged-topics-before/add-listener.hbs @@ -0,0 +1 @@ +{{flagged-topic-listener flaggedTopics=flaggedTopics}} diff --git a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 index 0a378b9..759475e 100644 --- a/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 +++ b/assets/javascripts/discourse-assign/initializers/extend-for-assigns.js.es6 @@ -6,6 +6,8 @@ import { observes } from 'ember-addons/ember-computed-decorators'; import Topic from 'discourse/models/topic'; import TopicFooterDropdown from 'discourse/components/topic-footer-mobile-dropdown'; import showModal from 'discourse/lib/show-modal'; +import { iconNode } from 'discourse-common/lib/icon-library'; +import { h } from 'virtual-dom'; function initialize(api, container) { @@ -84,16 +86,28 @@ function initialize(api, container) { }); } + api.createWidget('assigned-to', { + html(attrs) { + let { assignedToUser, href } = attrs; + + return h('p.assigned-to', [ + iconNode('user-plus'), + h('span.assign-text', I18n.t('discourse_assign.assigned_to')), + h('a', { attributes: { class: 'assigned-to-username', href } }, assignedToUser.username) + ]); + } + }); + api.decorateWidget('post-contents:after-cooked', dec => { if (dec.attrs.post_number === 1) { const postModel = dec.getModel(); if (postModel) { const assignedToUser = postModel.get('topic.assigned_to_user'); if (assignedToUser) { - const path = postModel.get('topic.assignedToUserPath'); - const userLink = `${assignedToUser.username}`; - const html = I18n.t('discourse_assign.assign_html', {userLink}); - return dec.rawHtml(html); + return dec.widget.attach('assigned-to', { + assignedToUser, + href: postModel.get('topic.assignedToUserPath') + }); } } } diff --git a/assets/javascripts/discourse/components/claim-topic.js.es6 b/assets/javascripts/discourse/components/claim-topic.js.es6 new file mode 100644 index 0000000..e8916be --- /dev/null +++ b/assets/javascripts/discourse/components/claim-topic.js.es6 @@ -0,0 +1,44 @@ +import { ajax } from 'discourse/lib/ajax'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Component.extend({ + tagName: '', + claiming: false, + unassigning: false, + + actions: { + unassign() { + this.set('unassigning', true); + return ajax("/assign/unassign", { + type: 'PUT', + data: { topic_id: this.get('topic.id') } + }).then(() => { + this.set('topic.assigned_to_user', null); + }).catch(popupAjaxError).finally(() => { + this.set('unassigning', false); + }); + }, + + claim() { + this.set('claiming', true); + + let topic = this.get('topic'); + ajax(`/assign/claim/${topic.id}`, { + method: 'PUT' + }).then(() => { + this.set('topic.assigned_to_user', this.currentUser); + }).catch(e => { + if (e.jqXHR && e.jqXHR.responseJSON) { + let json = e.jqXHR.responseJSON; + if (json && json.extras) { + this.set('topic.assigned_to_user', json.extras.assigned_to); + } + } + return popupAjaxError(e); + }).finally(() => { + if (this.isDestroying || this.isDestroyed) { return; } + this.set('claiming', false); + }); + } + } +}); diff --git a/assets/javascripts/discourse/components/flagged-topic-listener.js.es6 b/assets/javascripts/discourse/components/flagged-topic-listener.js.es6 new file mode 100644 index 0000000..75b3e63 --- /dev/null +++ b/assets/javascripts/discourse/components/flagged-topic-listener.js.es6 @@ -0,0 +1,25 @@ +function assignIfEqual(topic, data) { + if (topic && topic.id === data.topic_id) { + Ember.set(topic, 'assigned_to_user', data.assigned_to); + } +} + +export default Ember.Component.extend({ + didInsertElement() { + this._super(); + this.messageBus.subscribe("/staff/topic-assignment", data => { + let flaggedTopics = this.get('flaggedTopics'); + if (flaggedTopics) { + flaggedTopics.forEach(ft => assignIfEqual(ft.topic, data)); + } else { + assignIfEqual(this.get('topic'), data); + } + }); + }, + + willDestroyElement() { + this._super(); + this.messageBus.unsubscribe("/staff/topic-assignment"); + } +}); + diff --git a/assets/javascripts/discourse/templates/components/claim-topic.hbs b/assets/javascripts/discourse/templates/components/claim-topic.hbs new file mode 100644 index 0000000..956d23a --- /dev/null +++ b/assets/javascripts/discourse/templates/components/claim-topic.hbs @@ -0,0 +1,21 @@ +{{#if topic.assigned_to_user}} +
+ {{avatar topic.assigned_to_user imageSize="small"}} + + {{topic.assigned_to_user.username}} + + {{d-button + icon="times" + class="btn-small unassign" + action=(action "unassign") + disabled=unassigning + title="discourse_assign.unassign.help"}} +
+{{else}} + {{d-button class="btn-small assign" + icon="user-plus" + action=(action "claim") + disabled=claiming + label="discourse_assign.claim.title" + title="discourse_assign.claim.help"}} +{{/if}} diff --git a/assets/stylesheets/assigns.scss b/assets/stylesheets/assigns.scss index fa6ade2..414fdef 100644 --- a/assets/stylesheets/assigns.scss +++ b/assets/stylesheets/assigns.scss @@ -1,6 +1,29 @@ -a.assigned-to .fa.fa-user-plus { - margin-right: 2px; - color: #999; +.assigned-to { + .d-icon, i.fa { + margin-right: 0.5em; + color: $primary-medium; + } + .assign-text { + margin-right: 0.25em; + } +} + +.assigned-to-user { + display: flex; + align-items: center; + + img.avatar { + margin-right: 0.3em; + } + + .unassign { + margin-left: 0.5em; + } +} + +.topic-assigned-to { + min-width: 15%; + width: 15%; } .list-tags.assigned { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 59ce115..60117b8 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -5,7 +5,7 @@ en: unassigned: "unassigned %{who} %{when}" discourse_assign: assigned: "Assigned" - assign_html: "

Assigned to {{userLink}}

" + assigned_to: "Assigned to" assign_notification: "

{{username}} {{description}}

" unassign: title: "Unassign" @@ -17,3 +17,6 @@ en: title: "Assign Topic" description: "Enter the username of the person you'd like to assign this topic" assign: "Assign" + claim: + title: "claim" + help: "Assign topic to yourself" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 838b2a0..4275529 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -12,3 +12,4 @@ en: discourse_assign: assigned_to: "Topic assigned to @%{username}" unassigned: "Topic was unassigned" + already_claimed: "That topic has already been claimed." diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..e5404df --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,6 @@ +DiscourseAssign::Engine.routes.draw do + put "/claim/:topic_id" => "assign#claim" + put "/assign" => "assign#assign" + put "/unassign" => "assign#unassign" + get "/suggestions" => "assign#suggestions" +end diff --git a/lib/discourse_assign/engine.rb b/lib/discourse_assign/engine.rb new file mode 100644 index 0000000..ddfdc43 --- /dev/null +++ b/lib/discourse_assign/engine.rb @@ -0,0 +1,6 @@ +module ::DiscourseAssign + class Engine < ::Rails::Engine + engine_name "discourse_assign" + isolate_namespace DiscourseAssign + end +end diff --git a/lib/discourse_assign/helpers.rb b/lib/discourse_assign/helpers.rb new file mode 100644 index 0000000..4c8b882 --- /dev/null +++ b/lib/discourse_assign/helpers.rb @@ -0,0 +1,19 @@ +module DiscourseAssign + module Helpers + def self.build_assigned_to_user(assigned_to_user_id, topic) + if assigned_to_user_id && user = User.find_by(id: assigned_to_user_id) + assigned_at = TopicCustomField.where( + topic_id: topic.id, + name: "assigned_to_id" + ).pluck(:created_at).first + + { + username: user.username, + name: user.name, + avatar_template: user.avatar_template, + assigned_at: assigned_at + } + end + end + end +end diff --git a/lib/topic_assigner.rb b/lib/topic_assigner.rb new file mode 100644 index 0000000..886895e --- /dev/null +++ b/lib/topic_assigner.rb @@ -0,0 +1,202 @@ +class ::TopicAssigner + def self.backfill_auto_assign + staff_mention = User.where('moderator OR admin') + .pluck('username') + .map { |name| "p.cooked ILIKE '%mention%@#{name}%'" } + .join(' OR ') + + sql = < assign_to.username } + ) + + unless @assigned_by.id == assign_to.id + + Notification.create!( + notification_type: Notification.types[:custom], + user_id: assign_to.id, + topic_id: @topic.id, + post_number: 1, + data: { + message: 'discourse_assign.assign_notification', + display_username: @assigned_by.username, + topic_title: @topic.title + }.to_json + ) + end + end + + true + end + + 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! + + post = @topic.posts.where(post_number: 1).first + post.publish_change_to_clients!(:revised, reload_topic: true) + + assigned_user = User.find_by(id: assigned_to_id) + MessageBus.publish( + "/staff/topic-assignment", + { + type: 'unassigned', + topic_id: @topic.id, + }, + user_ids: staff_ids + ) + + UserAction.where( + action_type: UserAction::ASSIGNED, + target_post_id: post.id + ).destroy_all + + # yank notification + Notification.where( + notification_type: Notification.types[:custom], + user_id: assigned_user.try(:id), + topic_id: @topic.id, + post_number: 1 + ).where("data like '%discourse_assign.assign_notification%'").destroy_all + + if SiteSetting.unassign_creates_tracking_post && !silent + post_type = SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper] + @topic.add_moderator_post( + @assigned_by, nil, + bump: false, + post_type: post_type, + custom_fields: { "action_code_who" => assigned_user&.username }, + action_code: "unassigned" + ) + end + end + end +end diff --git a/plugin.rb b/plugin.rb index bb69d21..66ff9ee 100644 --- a/plugin.rb +++ b/plugin.rb @@ -6,392 +6,146 @@ enabled_site_setting :assign_enabled register_asset 'stylesheets/assigns.scss' +load File.expand_path('../lib/discourse_assign/engine.rb', __FILE__) +load File.expand_path('../lib/discourse_assign/helpers.rb', __FILE__) + +Discourse::Application.routes.append do + mount ::DiscourseAssign::Engine, at: "/assign" + get "topics/private-messages-assigned/:username" => "list#private_messages_assigned", as: "topics_private_messages_assigned", constraints: { username: /[\w.\-]+?/ } +end after_initialize do + require 'topic_assigner' - module ::DiscourseAssign - class Engine < ::Rails::Engine - engine_name "discourse_assign" - isolate_namespace DiscourseAssign - end - end - - class ::TopicAssigner - - def self.backfill_auto_assign - staff_mention = User.where('moderator OR admin') - .pluck('username') - .map { |name| "p.cooked ILIKE '%mention%@#{name}%'" } - .join(' OR ') - - sql = < assign_to.username} - }) - - unless @assigned_by.id == assign_to.id - - Notification.create!(notification_type: Notification.types[:custom], - user_id: assign_to.id, - topic_id: @topic.id, - post_number: 1, - data: { - message: 'discourse_assign.assign_notification', - display_username: @assigned_by.username, - topic_title: @topic.title - }.to_json - ) - end - end - - true - end - - 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! - - post = @topic.posts.where(post_number: 1).first - post.publish_change_to_clients!(:revised, { reload_topic: true }) - - assigned_user = User.find_by(id: assigned_to_id) - - UserAction.where( - action_type: UserAction::ASSIGNED, - target_post_id: post.id - ).destroy_all - - # yank notification - Notification.where( - notification_type: Notification.types[:custom], - user_id: assigned_user.try(:id), - topic_id: @topic.id, - post_number: 1 - ).where("data like '%discourse_assign.assign_notification%'") - .destroy_all - - if SiteSetting.unassign_creates_tracking_post && !silent - post_type = SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper] - @topic.add_moderator_post(@assigned_by, nil, - { bump: false, - post_type: post_type, - custom_fields: {"action_code_who" => assigned_user&.username}, - action_code: "unassigned"}) - end - end - end - end - - class ::DiscourseAssign::AssignController < Admin::AdminController - before_action :ensure_logged_in - - def suggestions - users = [current_user] - users += User - .where('admin OR moderator') - .where('users.id <> ?', current_user.id) - .joins("join ( - SELECT value::integer user_id, MAX(created_at) last_assigned - FROM topic_custom_fields - WHERE name = 'assigned_to_id' - GROUP BY value::integer - ) as X ON X.user_id = users.id") - .order('X.last_assigned DESC') - .limit(6) - - render json: ActiveModel::ArraySerializer.new(users, - scope: guardian, each_serializer: BasicUserSerializer) - end - - def unassign - topic_id = params.require(:topic_id) - topic = Topic.find(topic_id.to_i) - assigner = TopicAssigner.new(topic, current_user) - assigner.unassign - - render json: success_json - end - - def assign - topic_id = params.require(:topic_id) - username = params.require(:username) - - topic = Topic.find(topic_id.to_i) - assign_to = User.find_by(username_lower: username.downcase) - - raise Discourse::NotFound unless assign_to - - assigner = TopicAssigner.new(topic, current_user) - - # perhaps? - #Scheduler::Defer.later "assign topic" do - assigner.assign(assign_to) - - render json: success_json - end - - class ::Topic - def assigned_to_user - @assigned_to_user || - if user_id = custom_fields["assigned_to_id"] - @assigned_to_user = User.find_by(id: user_id) - end - end - - def preload_assigned_to_user(assigned_to_user) - @assigned_to_user = assigned_to_user - end - end - + # We can remove this check once this method is stable + if respond_to?(:add_preloaded_topic_list_custom_field) + add_preloaded_topic_list_custom_field('assigned_to_id') + else TopicList.preloaded_custom_fields << "assigned_to_id" + end - TopicList.on_preload do |topics, topic_list| - is_staff = topic_list.current_user && topic_list.current_user.staff? - allowed_access = SiteSetting.assigns_public || is_staff + TopicList.on_preload do |topics, topic_list| + is_staff = topic_list.current_user && topic_list.current_user.staff? + allowed_access = SiteSetting.assigns_public || is_staff - if allowed_access && topics.length > 0 - users = User.where("users.id in ( - SELECT value::int - FROM topic_custom_fields - WHERE name = 'assigned_to_id' AND topic_id IN (?) - )", topics.map(&:id)) - .joins('join user_emails on user_emails.user_id = users.id AND user_emails.primary') - .select(:id, 'user_emails.email', :username, :uploaded_avatar_id) + if allowed_access && topics.length > 0 + users = User.where("users.id in ( + SELECT value::int + FROM topic_custom_fields + WHERE name = 'assigned_to_id' AND topic_id IN (?) + )", topics.map(&:id)) + .joins('join user_emails on user_emails.user_id = users.id AND user_emails.primary') + .select(:id, 'user_emails.email', :username, :uploaded_avatar_id) - map = {} - users.each { |u| map[u.id] = u } + map = {} + users.each { |u| map[u.id] = u } - topics.each do |t| - if id = t.custom_fields['assigned_to_id'] - t.preload_assigned_to_user(map[id.to_i]) - end + topics.each do |t| + if id = t.custom_fields['assigned_to_id'] + t.preload_assigned_to_user(map[id.to_i]) end end end + end - require_dependency 'topic_query' - TopicQuery.add_custom_filter(:assigned) do |results, topic_query| - if topic_query.guardian.is_staff? || SiteSetting.assigns_public - username = topic_query.options[:assigned] + require_dependency 'topic_query' + TopicQuery.add_custom_filter(:assigned) do |results, topic_query| + if topic_query.guardian.is_staff? || SiteSetting.assigns_public + username = topic_query.options[:assigned] - user_id = topic_query.guardian.user.id if username == "me" + user_id = topic_query.guardian.user.id if username == "me" - special = ["*", "nobody"].include?(username) + special = ["*", "nobody"].include?(username) - if username.present? && !special - user_id ||= User.where(username_lower: username.downcase).pluck(:id).first - end + if username.present? && !special + user_id ||= User.where(username_lower: username.downcase).pluck(:id).first + end - if user_id || special + if user_id || special - if username == "nobody" - results = results.joins("LEFT JOIN topic_custom_fields tc_assign ON - topics.id = tc_assign.topic_id AND - tc_assign.name = 'assigned_to_id'") - .where("tc_assign.name IS NULL") + if username == "nobody" + results = results.joins("LEFT JOIN topic_custom_fields tc_assign ON + topics.id = tc_assign.topic_id AND + tc_assign.name = 'assigned_to_id'") + .where("tc_assign.name IS NULL") + else + + if username == "*" + filter = "AND tc_assign.value IS NOT NULL" else - - if username == "*" - filter = "AND tc_assign.value IS NOT NULL" - else - filter = "AND tc_assign.value = '#{user_id.to_i.to_s}'" - end - - results = results.joins("JOIN topic_custom_fields tc_assign ON - topics.id = tc_assign.topic_id AND - tc_assign.name = 'assigned_to_id' - #{filter} - ") + filter = "AND tc_assign.value = '#{user_id.to_i.to_s}'" end + + results = results.joins("JOIN topic_custom_fields tc_assign ON + topics.id = tc_assign.topic_id AND + tc_assign.name = 'assigned_to_id' + #{filter} + ") end end - - results end - require_dependency 'topic_list_item_serializer' - class ::TopicListItemSerializer - has_one :assigned_to_user, serializer: BasicUserSerializer, embed: :objects + results + end - def include_assigned_to_user? - (SiteSetting.assigns_public || scope.is_staff?) && object.assigned_to_user + require_dependency 'topic_list_item_serializer' + class ::TopicListItemSerializer + has_one :assigned_to_user, serializer: BasicUserSerializer, embed: :objects + end + + require_dependency 'list_controller' + class ::ListController + generate_message_route(:private_messages_assigned) + end + + add_to_class(:topic_query, :list_private_messages_assigned) do |user| + list = private_messages_for(user, :all) + list = list.where("topics.id IN ( + SELECT topic_id FROM topic_custom_fields WHERE name = 'assigned_to_id' AND value = ? + )", user.id.to_s) + create_list(:private_messages, {}, list) + end + + add_to_class(:topic, :assigned_to_user) do + @assigned_to_user || + if user_id = custom_fields["assigned_to_id"] + @assigned_to_user = User.find_by(id: user_id) end + end + + add_to_class(:topic, :preload_assigned_to_user) do |assigned_to_user| + @assigned_to_user = assigned_to_user + end + + add_to_serializer(:topic_list_item, 'include_assigned_to_user?') do + (SiteSetting.assigns_public || scope.is_staff?) && object.assigned_to_user + end + + add_to_serializer(:topic_view, :assigned_to_user, false) do + DiscourseAssign::Helpers.build_assigned_to_user(assigned_to_user_id, object.topic) + end + + add_to_class(:topic_view_serializer, :assigned_to_user_id) do + id = object.topic.custom_fields["assigned_to_id"] + # a bit messy but race conditions can give us an array here, avoid + id && id.to_i rescue nil + end + + add_to_serializer(:topic_view, 'include_assigned_to_user?') do + if SiteSetting.assigns_public || scope.is_staff? + # subtle but need to catch cases where stuff is not assigned + object.topic.custom_fields.keys.include?("assigned_to_id") end + end - require_dependency 'topic_view_serializer' - class ::TopicViewSerializer - attributes :assigned_to_user + add_to_serializer(:flagged_topic, :assigned_to_user) do + DiscourseAssign::Helpers.build_assigned_to_user(assigned_to_user_id, object) + end - def assigned_to_user - if assigned_to_user_id && user = User.find_by(id: assigned_to_user_id) - - assigned_at = TopicCustomField.where( - topic_id: object.topic.id, - name: "assigned_to_id" - ).pluck(:created_at).first - - { - username: user.username, - name: user.name, - avatar_template: user.avatar_template, - assigned_at: assigned_at - } - end - end - - def include_assigned_to_user? - if SiteSetting.assigns_public || scope.is_staff? - # subtle but need to catch cases where stuff is not assigned - object.topic.custom_fields.keys.include?("assigned_to_id") - end - end - - def assigned_to_user_id - id = object.topic.custom_fields["assigned_to_id"] - # a bit messy but race conditions can give us an array here, avoid - id && id.to_i rescue nil - end - end - - require_dependency 'topic_query' - class ::TopicQuery - def list_private_messages_assigned(user) - list = private_messages_for(user, :all) - list = list.where("topics.id IN ( - SELECT topic_id FROM topic_custom_fields WHERE name = 'assigned_to_id' AND value = ? - )", user.id.to_s) - create_list(:private_messages, {}, list) - end - end - - require_dependency 'list_controller' - class ::ListController - generate_message_route(:private_messages_assigned) - end - - DiscourseAssign::Engine.routes.draw do - put "/assign" => "assign#assign" - put "/unassign" => "assign#unassign" - get "/suggestions" => "assign#suggestions" - end - - Discourse::Application.routes.append do - mount ::DiscourseAssign::Engine, at: "/assign" - get "topics/private-messages-assigned/:username" => "list#private_messages_assigned", as: "topics_private_messages_assigned", constraints: { username: /[\w.\-]+?/ } - end + add_to_serializer(:flagged_topic, :assigned_to_user_id) do + id = object.custom_fields["assigned_to_id"] + # a bit messy but race conditions can give us an array here, avoid + id && id.to_i rescue nil end on(:post_created) do |post|