FEATURE: Add assign note (#326)

This commit is contained in:
Natalie Tay 2022-05-05 13:21:14 +08:00 committed by GitHub
parent 3151bea1da
commit 1ac2dfbae2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 179 additions and 85 deletions

View File

@ -48,6 +48,7 @@ module DiscourseAssign
target_type = params.require(:target_type) target_type = params.require(:target_type)
username = params.permit(:username)['username'] username = params.permit(:username)['username']
group_name = params.permit(:group_name)['group_name'] group_name = params.permit(:group_name)['group_name']
note = params.permit(:note)['note'].presence
assign_to = username.present? ? User.find_by(username_lower: username.downcase) : Group.where("LOWER(name) = ?", group_name.downcase).first assign_to = username.present? ? User.find_by(username_lower: username.downcase) : Group.where("LOWER(name) = ?", group_name.downcase).first
@ -56,7 +57,7 @@ module DiscourseAssign
target = target_type.constantize.where(id: target_id).first target = target_type.constantize.where(id: target_id).first
raise Discourse::NotFound unless target raise Discourse::NotFound unless target
assign = Assigner.new(target, current_user).assign(assign_to) assign = Assigner.new(target, current_user).assign(assign_to, note: note)
if assign[:success] if assign[:success]
render json: success_json render json: success_json

View File

@ -37,6 +37,7 @@ end
# target_id :integer not null # target_id :integer not null
# target_type :string not null # target_type :string not null
# active :boolean default(TRUE) # active :boolean default(TRUE)
# note :string
# #
# Indexes # Indexes
# #

View File

@ -1,10 +1,10 @@
import Controller, { inject as controller } from "@ember/controller"; import Controller, { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { not, or } from "@ember/object/computed";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import { ajax } from "discourse/lib/ajax"; import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error"; import { popupAjaxError } from "discourse/lib/ajax-error";
import { not, or } from "@ember/object/computed";
import { isEmpty } from "@ember/utils";
import { action } from "@ember/object";
export default Controller.extend({ export default Controller.extend({
topicBulkActions: controller(), topicBulkActions: controller(),
@ -71,6 +71,7 @@ export default Controller.extend({
group_name: this.get("model.group_name"), group_name: this.get("model.group_name"),
target_id: this.get("model.target.id"), target_id: this.get("model.target.id"),
target_type: this.get("model.targetType"), target_type: this.get("model.targetType"),
note: this.get("model.note"),
}, },
}) })
.then(() => { .then(() => {
@ -99,10 +100,6 @@ export default Controller.extend({
"model.allowedGroups": this.taskActions.allowedGroups, "model.allowedGroups": this.taskActions.allowedGroups,
}); });
} }
if (name) {
return this.assign();
}
}, },
@action @action

View File

@ -768,6 +768,8 @@ function initialize(api) {
post = topic.postStream.posts.find((p) => p.id === data.post_id); post = topic.postStream.posts.find((p) => p.id === data.post_id);
} }
const target = post || topic; const target = post || topic;
target.set("assignment_note", null);
if (data.assigned_type === "User") { if (data.assigned_type === "User") {
target.set( target.set(
"assigned_to_user_id", "assigned_to_user_id",

View File

@ -38,6 +38,7 @@ export default Service.extend({
group_name: target.assigned_to_group?.name, group_name: target.assigned_to_group?.name,
target, target,
targetType: options.targetType, targetType: options.targetType,
note: target.assignment_note,
}, },
}); });
}, },
@ -49,6 +50,7 @@ export default Service.extend({
username: user.username, username: user.username,
target_id: target.id, target_id: target.id,
target_type: targetType, target_type: targetType,
note: target.assignment_note,
}, },
}); });
}, },

View File

@ -25,6 +25,8 @@
</a> </a>
{{/each}} {{/each}}
</div> </div>
<label>{{i18n "discourse_assign.assign_modal.note_label"}}</label>
{{textarea value=model.note}}
</div> </div>
{{/d-modal-body}} {{/d-modal-body}}

View File

@ -78,6 +78,8 @@
.assign-suggestions { .assign-suggestions {
margin-top: 15px; margin-top: 15px;
margin-bottom: 15px;
img { img {
margin-right: 5px; margin-right: 5px;
cursor: pointer; cursor: pointer;

View File

@ -53,6 +53,7 @@ en:
reassign_title: "Reassign Topic" reassign_title: "Reassign Topic"
description: "Enter the name of the user you'd like to assign this topic" description: "Enter the name of the user you'd like to assign this topic"
assign: "Assign" assign: "Assign"
note_label: Note
assign_post_modal: assign_post_modal:
title: "Assign Post" title: "Assign Post"
description: "Enter the name of the user you'd like to assign this post" description: "Enter the name of the user you'd like to assign this post"

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddNoteToAssignments < ActiveRecord::Migration[6.1]
def change
add_column :assignments, :note, :string, null: true
end
end

View File

@ -180,7 +180,7 @@ class ::Assigner
topic.posts.where(post_number: 1).first topic.posts.where(post_number: 1).first
end end
def forbidden_reasons(assign_to:, type:) def forbidden_reasons(assign_to:, type:, note:)
case case
when assign_to.is_a?(User) && !can_assignee_see_target?(assign_to) when assign_to.is_a?(User) && !can_assignee_see_target?(assign_to)
topic.private_message? ? :forbidden_assignee_not_pm_participant : :forbidden_assignee_cant_see_topic topic.private_message? ? :forbidden_assignee_not_pm_participant : :forbidden_assignee_cant_see_topic
@ -188,9 +188,9 @@ class ::Assigner
topic.private_message? ? :forbidden_group_assignee_not_pm_participant : :forbidden_group_assignee_cant_see_topic topic.private_message? ? :forbidden_group_assignee_not_pm_participant : :forbidden_group_assignee_cant_see_topic
when !can_be_assigned?(assign_to) when !can_be_assigned?(assign_to)
assign_to.is_a?(User) ? :forbidden_assign_to : :forbidden_group_assign_to assign_to.is_a?(User) ? :forbidden_assign_to : :forbidden_group_assign_to
when topic.assignment&.assigned_to_id == assign_to.id && topic.assignment&.assigned_to_type == type && topic.assignment.active == true when topic_same_assignee_and_note(assign_to, type, note)
assign_to.is_a?(User) ? :already_assigned : :group_already_assigned assign_to.is_a?(User) ? :already_assigned : :group_already_assigned
when @target.is_a?(Topic) && Assignment.where(topic_id: topic.id, target_type: "Post", active: true).any? { |assignment| assignment.assigned_to_id == assign_to.id && assignment.assigned_to_type == type } when post_same_assignee_and_note(assign_to, type, note)
assign_to.is_a?(User) ? :already_assigned : :group_already_assigned assign_to.is_a?(User) ? :already_assigned : :group_already_assigned
when Assignment.where(topic: topic).count >= ASSIGNMENTS_PER_TOPIC_LIMIT when Assignment.where(topic: topic).count >= ASSIGNMENTS_PER_TOPIC_LIMIT
:too_many_assigns_for_topic :too_many_assigns_for_topic
@ -199,10 +199,10 @@ class ::Assigner
end end
end end
def assign(assign_to, silent: false) def assign(assign_to, note: nil, silent: false)
type = assign_to.is_a?(User) ? "User" : "Group" type = assign_to.is_a?(User) ? "User" : "Group"
forbidden_reason = forbidden_reasons(assign_to: assign_to, type: type) forbidden_reason = forbidden_reasons(assign_to: assign_to, type: type, note: note)
return { success: false, reason: forbidden_reason } if forbidden_reason return { success: false, reason: forbidden_reason } if forbidden_reason
action_code = {} action_code = {}
@ -211,7 +211,7 @@ class ::Assigner
@target.assignment&.destroy! @target.assignment&.destroy!
assignment = @target.create_assignment!(assigned_to_id: assign_to.id, assigned_to_type: type, assigned_by_user_id: @assigned_by.id, topic_id: topic.id) assignment = @target.create_assignment!(assigned_to_id: assign_to.id, assigned_to_type: type, assigned_by_user_id: @assigned_by.id, topic_id: topic.id, note: note)
first_post.publish_change_to_clients!(:revised, reload_topic: true) first_post.publish_change_to_clients!(:revised, reload_topic: true)
@ -413,4 +413,20 @@ class ::Assigner
return "unassigned#{suffix}" if assignment.assigned_to_user? return "unassigned#{suffix}" if assignment.assigned_to_user?
return "unassigned_group#{suffix}" if assignment.assigned_to_group? return "unassigned_group#{suffix}" if assignment.assigned_to_group?
end end
def topic_same_assignee_and_note(assign_to, type, note)
topic.assignment&.assigned_to_id == assign_to.id &&
topic.assignment&.assigned_to_type == type &&
topic.assignment.active == true &&
topic.assignment&.note == note
end
def post_same_assignee_and_note(assign_to, type, note)
@target.is_a?(Topic) &&
Assignment.where(topic_id: topic.id, target_type: "Post", active: true).any? do |assignment|
assignment.assigned_to_id == assign_to.id &&
assignment.assigned_to_type == type &&
assignment&.note == note
end
end
end end

View File

@ -484,6 +484,14 @@ after_initialize do
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.indirectly_assigned_to.present? (SiteSetting.assigns_public || scope.can_assign?) && object.topic.indirectly_assigned_to.present?
end end
add_to_serializer(:topic_view, :assignment_note, false) do
object.topic.assignment.note
end
add_to_serializer(:topic_view, :include_assignment_note?, false) do
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assignment.present?
end
# SuggestedTopic serializer # SuggestedTopic serializer
add_to_serializer(:suggested_topic, :assigned_to_user, false) do add_to_serializer(:suggested_topic, :assigned_to_user, false) do
DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object) DiscourseAssign::Helpers.build_assigned_to_user(object.assigned_to, object)
@ -631,6 +639,14 @@ after_initialize do
(SiteSetting.assigns_public || scope.can_assign?) && object.assignment&.assigned_to&.is_a?(Group) && object.assignment.active (SiteSetting.assigns_public || scope.can_assign?) && object.assignment&.assigned_to&.is_a?(Group) && object.assignment.active
end end
add_to_serializer(:post, :assignment_note, false) do
object.assignment.note
end
add_to_serializer(:post, :include_assignment_note?, false) do
(SiteSetting.assigns_public || scope.can_assign?) && object.assignment.present?
end
# CurrentUser serializer # CurrentUser serializer
add_to_serializer(:current_user, :can_assign) do add_to_serializer(:current_user, :can_assign) do
object.can_assign? object.can_assign?

View File

@ -43,6 +43,12 @@ RSpec.describe Assigner do
.to eq(TopicUser.notification_levels[:tracking]) .to eq(TopicUser.notification_levels[:tracking])
end end
it "can assign with note" do
assigner.assign(moderator, note: "tomtom best mom")
expect(topic.assignment.note).to eq "tomtom best mom"
end
it "publishes topic assignment after assign and unassign" do it "publishes topic assignment after assign and unassign" do
messages = MessageBus.track_publish('/staff/topic-assignment') do messages = MessageBus.track_publish('/staff/topic-assignment') do
assigner = described_class.new(topic, moderator_2) assigner = described_class.new(topic, moderator_2)
@ -139,86 +145,107 @@ RSpec.describe Assigner do
assigner.assign(assignee).fetch(:success) assigner.assign(assignee).fetch(:success)
end end
it "doesn't assign if the user has too many assigned topics" do
SiteSetting.max_assigned_topics = 1
another_post = Fabricate.build(:post)
assigner.assign(moderator)
second_assign = described_class.new(another_post.topic, moderator_2).assign(moderator)
expect(second_assign[:success]).to eq(false)
expect(second_assign[:reason]).to eq(:too_many_assigns)
end
it "doesn't enforce the limit when self-assigning" do
SiteSetting.max_assigned_topics = 1
another_post = Fabricate(:post)
assigner.assign(moderator)
second_assign = described_class.new(another_post.topic, moderator).assign(moderator)
expect(second_assign[:success]).to eq(true)
end
it "doesn't count self-assigns when enforcing the limit" do
SiteSetting.max_assigned_topics = 1
another_post = Fabricate(:post)
first_assign = assigner.assign(moderator)
# reached limit so stop
second_assign = described_class.new(Fabricate(:topic), moderator_2).assign(moderator)
# self assign has a bypass
third_assign = described_class.new(another_post.topic, moderator).assign(moderator)
expect(first_assign[:success]).to eq(true)
expect(second_assign[:success]).to eq(false)
expect(third_assign[:success]).to eq(true)
end
it "doesn't count inactive assigns when enforcing the limit" do
SiteSetting.max_assigned_topics = 1
SiteSetting.unassign_on_close = true
another_post = Fabricate(:post)
first_assign = assigner.assign(moderator)
topic.update_status("closed", true, Discourse.system_user)
second_assign = described_class.new(another_post.topic, moderator_2).assign(moderator)
expect(first_assign[:success]).to eq(true)
expect(second_assign[:success]).to eq(true)
end
fab!(:admin) { Fabricate(:admin) } fab!(:admin) { Fabricate(:admin) }
it 'fails to assign when the assigned user cannot view the pm' do context "forbidden reasons" do
assign = described_class.new(pm, admin).assign(moderator) it "doesn't assign if the user has too many assigned topics" do
SiteSetting.max_assigned_topics = 1
another_post = Fabricate.build(:post)
assigner.assign(moderator)
expect(assign[:success]).to eq(false) second_assign = described_class.new(another_post.topic, moderator_2).assign(moderator)
expect(assign[:reason]).to eq(:forbidden_assignee_not_pm_participant)
end
it 'fails to assign when not all group members has access to pm' do expect(second_assign[:success]).to eq(false)
assign = described_class.new(pm, admin).assign(moderator.groups.first) expect(second_assign[:reason]).to eq(:too_many_assigns)
end
expect(assign[:success]).to eq(false) it "doesn't enforce the limit when self-assigning" do
expect(assign[:reason]).to eq(:forbidden_group_assignee_not_pm_participant) SiteSetting.max_assigned_topics = 1
end another_post = Fabricate(:post)
assigner.assign(moderator)
it 'fails to assign when the assigned user cannot view the topic' do second_assign = described_class.new(another_post.topic, moderator).assign(moderator)
assign = described_class.new(secure_topic, admin).assign(moderator)
expect(assign[:success]).to eq(false) expect(second_assign[:success]).to eq(true)
expect(assign[:reason]).to eq(:forbidden_assignee_cant_see_topic) end
end
it 'fails to assign when the not all group members can view the topic' do it "doesn't count self-assigns when enforcing the limit" do
assign = described_class.new(secure_topic, admin).assign(moderator.groups.first) SiteSetting.max_assigned_topics = 1
another_post = Fabricate(:post)
expect(assign[:success]).to eq(false) first_assign = assigner.assign(moderator)
expect(assign[:reason]).to eq(:forbidden_group_assignee_cant_see_topic)
# reached limit so stop
second_assign = described_class.new(Fabricate(:topic), moderator_2).assign(moderator)
# self assign has a bypass
third_assign = described_class.new(another_post.topic, moderator).assign(moderator)
expect(first_assign[:success]).to eq(true)
expect(second_assign[:success]).to eq(false)
expect(third_assign[:success]).to eq(true)
end
it "doesn't count inactive assigns when enforcing the limit" do
SiteSetting.max_assigned_topics = 1
SiteSetting.unassign_on_close = true
another_post = Fabricate(:post)
first_assign = assigner.assign(moderator)
topic.update_status("closed", true, Discourse.system_user)
second_assign = described_class.new(another_post.topic, moderator_2).assign(moderator)
expect(first_assign[:success]).to eq(true)
expect(second_assign[:success]).to eq(true)
end
it 'fails to assign when the assigned user and note is the same' do
assigner = described_class.new(topic, admin)
assigner.assign(moderator, note: "note me down")
assign = assigner.assign(moderator, note: "note me down")
expect(assign[:success]).to eq(false)
expect(assign[:reason]).to eq(:already_assigned)
end
it 'allows assign when the assigned user is same but note is different' do
assigner = described_class.new(topic, admin)
assigner.assign(moderator, note: "note me down")
assign = assigner.assign(moderator, note: "note me down again")
expect(assign[:success]).to eq(true)
end
it 'fails to assign when the assigned user cannot view the pm' do
assign = described_class.new(pm, admin).assign(moderator)
expect(assign[:success]).to eq(false)
expect(assign[:reason]).to eq(:forbidden_assignee_not_pm_participant)
end
it 'fails to assign when not all group members has access to pm' do
assign = described_class.new(pm, admin).assign(moderator.groups.first)
expect(assign[:success]).to eq(false)
expect(assign[:reason]).to eq(:forbidden_group_assignee_not_pm_participant)
end
it 'fails to assign when the assigned user cannot view the topic' do
assign = described_class.new(secure_topic, admin).assign(moderator)
expect(assign[:success]).to eq(false)
expect(assign[:reason]).to eq(:forbidden_assignee_cant_see_topic)
end
it 'fails to assign when the not all group members can view the topic' do
assign = described_class.new(secure_topic, admin).assign(moderator.groups.first)
expect(assign[:success]).to eq(false)
expect(assign[:reason]).to eq(:forbidden_group_assignee_cant_see_topic)
end
end end
it "assigns the PM to the moderator when it's included in the list of allowed users" do it "assigns the PM to the moderator when it's included in the list of allowed users" do

View File

@ -116,6 +116,14 @@ RSpec.describe DiscourseAssign::AssignController do
expect(post.topic.reload.assignment.assigned_to_id).to eq(user2.id) expect(post.topic.reload.assignment.assigned_to_id).to eq(user2.id)
end end
it 'assigns topic with note to a user' do
put '/assign/assign.json', params: {
target_id: post.topic_id, target_type: 'Topic', username: user2.username, note: "do dis pls"
}
expect(post.topic.reload.assignment.note).to eq("do dis pls")
end
it 'assigns topic to a group' do it 'assigns topic to a group' do
put '/assign/assign.json', params: { put '/assign/assign.json', params: {
target_id: post.topic_id, target_type: 'Topic', group_name: assign_allowed_group.name target_id: post.topic_id, target_type: 'Topic', group_name: assign_allowed_group.name

View File

@ -33,4 +33,10 @@ describe PostSerializer do
expect(post[:assigned_to_group][:assign_icon]).to eq("group-plus") expect(post[:assigned_to_group][:assign_icon]).to eq("group-plus")
expect(post[:assigned_to_user]).to be(nil) expect(post[:assigned_to_user]).to be(nil)
end end
it "includes note in serializer" do
Assigner.new(post, user).assign(user, note: "tomtom best")
serializer = PostSerializer.new(post, scope: guardian)
expect(serializer.as_json[:post][:assignment_note]).to eq("tomtom best")
end
end end

View File

@ -29,4 +29,10 @@ RSpec.describe TopicViewSerializer do
expect(serializer.as_json[:topic_view][:assigned_to_group][:name]).to eq(assign_allowed_group.name) expect(serializer.as_json[:topic_view][:assigned_to_group][:name]).to eq(assign_allowed_group.name)
expect(serializer.as_json[:topic_view][:assigned_to_user]).to be nil expect(serializer.as_json[:topic_view][:assigned_to_user]).to be nil
end end
it "includes note in serializer" do
Assigner.new(topic, user).assign(user, note: "note me down")
serializer = TopicViewSerializer.new(TopicView.new(topic), scope: guardian)
expect(serializer.as_json[:topic_view][:assignment_note]).to eq("note me down")
end
end end