FEATURE: assign to post (#224)

Ability to assign an individual post to a user or group
This commit is contained in:
Krzysztof Kotlarek 2021-11-01 09:27:13 +11:00 committed by GitHub
parent 54c670536c
commit c396605d2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 439 additions and 140 deletions

View File

@ -133,24 +133,24 @@ module DiscourseAssign
SQL SQL
end end
group_assignment_count = Topic group_assignments = Topic
.joins("JOIN assignments a ON a.target_id = topics.id AND a.target_type = 'Topic'") .joins("JOIN assignments a ON a.topic_id = topics.id")
.where(<<~SQL, group_id: group.id) .where(<<~SQL, group_id: group.id)
a.assigned_to_id = :group_id AND a.assigned_to_type = 'Group' a.assigned_to_id = :group_id AND a.assigned_to_type = 'Group'
SQL SQL
.count .pluck(:topic_id)
assignment_count = Topic assignments = Topic
.joins("JOIN assignments a ON a.target_id = topics.id AND a.target_type = 'Topic'") .joins("JOIN assignments a ON a.topic_id = topics.id")
.joins("JOIN group_users ON group_users.user_id = a.assigned_to_id ") .joins("JOIN group_users ON group_users.user_id = a.assigned_to_id ")
.where("group_users.group_id = ?", group.id) .where("group_users.group_id = ?", group.id)
.where("a.assigned_to_type = 'User'") .where("a.assigned_to_type = 'User'")
.count .pluck(:topic_id)
render json: { render json: {
members: serialize_data(members, GroupUserAssignedSerializer), members: serialize_data(members, GroupUserAssignedSerializer),
assignment_count: assignment_count + group_assignment_count, assignment_count: (assignments | group_assignments).count,
group_assignment_count: group_assignment_count group_assignment_count: group_assignments.count
} }
end end
@ -174,6 +174,8 @@ module DiscourseAssign
{ error: I18n.t('discourse_assign.forbidden_group_assignee_not_pm_participant', group: assign_to.name) } { error: I18n.t('discourse_assign.forbidden_group_assignee_not_pm_participant', group: assign_to.name) }
when :forbidden_group_assignee_cant_see_topic when :forbidden_group_assignee_cant_see_topic
{ error: I18n.t('discourse_assign.forbidden_group_assignee_cant_see_topic', group: assign_to.name) } { error: I18n.t('discourse_assign.forbidden_group_assignee_cant_see_topic', group: assign_to.name) }
when :too_many_assigns_for_topic
{ error: I18n.t('discourse_assign.too_many_assigns_for_topic', limit: Assigner::ASSIGNMENTS_PER_TOPIC_LIMIT) }
else else
max = SiteSetting.max_assigned_topics max = SiteSetting.max_assigned_topics
{ error: I18n.t('discourse_assign.too_many_assigns', username: assign_to.username, max: max) } { error: I18n.t('discourse_assign.too_many_assigns', username: assign_to.username, max: max) }

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Assignment < ActiveRecord::Base class Assignment < ActiveRecord::Base
VALID_TYPES = %w(topic).freeze VALID_TYPES = %w(topic post).freeze
belongs_to :topic belongs_to :topic
belongs_to :assigned_to, polymorphic: true belongs_to :assigned_to, polymorphic: true

View File

@ -5,6 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { not, or } from "@ember/object/computed"; import { not, or } from "@ember/object/computed";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";
import { action } from "@ember/object"; import { action } from "@ember/object";
import discourseComputed from "discourse-common/utils/decorators";
export default Controller.extend({ export default Controller.extend({
topicBulkActions: controller(), topicBulkActions: controller(),
@ -38,6 +39,16 @@ export default Controller.extend({
}); });
}, },
@discourseComputed("model.targetType")
i18nSuffix(targetType) {
switch (targetType) {
case "Post":
return "_post_modal";
case "Topic":
return "_modal";
}
},
@action @action
assignUser(name) { assignUser(name) {
if (this.isBulkAction) { if (this.isBulkAction) {
@ -73,11 +84,11 @@ export default Controller.extend({
let path = "/assign/assign"; let path = "/assign/assign";
if (isEmpty(this.get("model.username"))) { if (isEmpty(this.get("model.username"))) {
this.model.topic.set("assigned_to_user", null); this.model.target.set("assigned_to_user", null);
} }
if (isEmpty(this.get("model.group_name"))) { if (isEmpty(this.get("model.group_name"))) {
this.model.topic.set("assigned_to_group", null); this.model.target.set("assigned_to_group", null);
} }
if ( if (
@ -94,8 +105,8 @@ export default Controller.extend({
data: { data: {
username: this.get("model.username"), username: this.get("model.username"),
group_name: this.get("model.group_name"), group_name: this.get("model.group_name"),
target_id: this.get("model.topic.id"), target_id: this.get("model.target.id"),
target_type: "Topic", target_type: this.get("model.targetType"),
}, },
}) })
.then(() => { .then(() => {

View File

@ -48,9 +48,9 @@ export default UserTopicsList.extend({
}, },
@action @action
unassign(topic) { unassign(targetId, targetType = "Topic") {
this.taskActions this.taskActions
.unassign(topic.get("id")) .unassign(targetId, targetType)
.then(() => this.send("changeAssigned")); .then(() => this.send("changeAssigned"));
}, },

View File

@ -58,9 +58,9 @@ export default UserTopicsList.extend({
}, },
@action @action
unassign(topic) { unassign(targetId, targetType = "Topic") {
this.taskActions this.taskActions
.unassign(topic.get("id")) .unassign(targetId, targetType)
.then(() => this.send("changeAssigned")); .then(() => this.send("changeAssigned"));
}, },

View File

@ -95,7 +95,7 @@ function registerTopicFooterButtons(api) {
if (topic.assigned_to_user || topic.assigned_to_group) { if (topic.assigned_to_user || topic.assigned_to_group) {
this.set("topic.assigned_to_user", null); this.set("topic.assigned_to_user", null);
this.set("topic.assigned_to_group", null); this.set("topic.assigned_to_group", null);
taskActions.unassign(topic.id).then(() => { taskActions.unassign(topic.id, "Topic").then(() => {
this.appEvents.trigger("post-stream:refresh", { this.appEvents.trigger("post-stream:refresh", {
id: topic.postStream.firstPostId, id: topic.postStream.firstPostId,
}); });
@ -151,6 +151,38 @@ function initialize(api) {
}, },
before: "top", before: "top",
}); });
if (api.getCurrentUser() && api.getCurrentUser().can_assign) {
api.addPostMenuButton("assign", (post) => {
if (post.firstPost) {
return;
}
if (post.assigned_to_user || post.assigned_to_group) {
return {
action: "unassignPost",
icon: "user-times",
className: "unassign-post",
title: "discourse_assign.unassign_post.title",
position: "second-last-hidden",
};
} else {
return {
action: "assignPost",
icon: "user-plus",
className: "assign-post",
title: "discourse_assign.assign_post.title",
position: "second-last-hidden",
};
}
});
api.attachWidgetAction("post", "assignPost", function () {
const taskActions = getOwner(this).lookup("service:task-actions");
taskActions.assign(this.model, "Post");
});
api.attachWidgetAction("post", "unassignPost", function () {
const taskActions = getOwner(this).lookup("service:task-actions");
taskActions.unassign(this.model.id, "Post");
});
}
} }
api.addAdvancedSearchOptions( api.addAdvancedSearchOptions(
@ -170,52 +202,55 @@ function initialize(api) {
: {} : {}
); );
api.modifyClass("model:topic", { function assignedToUserPath(assignedToUser) {
pluginId: PLUGIN_ID, return getURL(
siteSettings.assigns_user_url_path.replace(
"{username}",
assignedToUser.username
)
);
}
@discourseComputed("assigned_to_user") function assignedToGroupPath(assignedToGroup) {
assignedToUserPath(assignedToUser) { return getURL(`/g/${assignedToGroup.name}/assigned/everyone`);
return getURL( }
siteSettings.assigns_user_url_path.replace(
"{username}",
assignedToUser.username
)
);
},
@discourseComputed("assigned_to_group")
assignedToGroupPath(assignedToGroup) {
return getURL(`/g/${assignedToGroup.name}/assigned/everyone`);
},
});
api.modifyClass("model:bookmark", { api.modifyClass("model:bookmark", {
pluginId: PLUGIN_ID, pluginId: PLUGIN_ID,
@discourseComputed("assigned_to_user") @discourseComputed("assigned_to_user")
assignedToUserPath(assignedToUser) { assignedToUserPath(assignedToUser) {
return getURL( return assignedToUserPath(assignedToUser);
this.siteSettings.assigns_user_url_path.replace(
"{username}",
assignedToUser.username
)
);
}, },
@discourseComputed("assigned_to_group") @discourseComputed("assigned_to_group")
assignedToGroupPath(assignedToGroup) { assignedToGroupPath(assignedToGroup) {
return getURL(`/g/${assignedToGroup.name}/assigned/everyone`); return assignedToGroupPath(assignedToGroup);
}, },
}); });
api.addPostSmallActionIcon("assigned", "user-plus"); api.addPostSmallActionIcon("assigned", "user-plus");
api.addPostSmallActionIcon("assigned_to_post", "user-plus");
api.addPostSmallActionIcon("assigned_group", "group-plus"); api.addPostSmallActionIcon("assigned_group", "group-plus");
api.addPostSmallActionIcon("assigned_group_to_post", "group-plus");
api.addPostSmallActionIcon("unassigned", "user-times"); api.addPostSmallActionIcon("unassigned", "user-times");
api.addPostSmallActionIcon("unassigned_group", "group-times"); api.addPostSmallActionIcon("unassigned_group", "group-times");
api.addPostSmallActionIcon("unassigned_from_post", "user-times");
api.addPostSmallActionIcon("unassigned_group_from_post", "group-times");
api.includePostAttributes("assigned_to_user", "assigned_to_group");
api.addPostTransformCallback((transformed) => { api.addPostTransformCallback((transformed) => {
if ( if (
["assigned", "unassigned", "assigned_group", "unassigned_group"].includes( [
transformed.actionCode "assigned",
) "unassigned",
"assigned_group",
"unassigned_group",
"assigned_to_post",
"assigned_group_to_post",
"unassigned_from_post",
"unassigned_group_from_post",
].includes(transformed.actionCode)
) { ) {
transformed.isSmallAction = true; transformed.isSmallAction = true;
transformed.canEdit = false; transformed.canEdit = false;
@ -225,22 +260,45 @@ function initialize(api) {
api.addDiscoveryQueryParam("assigned", { replace: true, refreshModel: true }); api.addDiscoveryQueryParam("assigned", { replace: true, refreshModel: true });
api.addTagsHtmlCallback((topic, params = {}) => { api.addTagsHtmlCallback((topic, params = {}) => {
const assignedToUser = topic.get("assigned_to_user.username"); const [
const assignedToGroup = topic.get("assigned_to_group.name"); assignedToUser,
if (assignedToUser || assignedToGroup) { assignedToGroup,
const assignedPath = assignedToUser assignedToIndirectly,
? topic.assignedToUserPath ] = Object.values(
: topic.assignedToGroupPath; topic.getProperties(
const tagName = params.tagName || "a"; "assigned_to_user",
const icon = assignedToUser "assigned_to_group",
? iconHTML("user-plus") "indirectly_assigned_to"
: iconHTML("group-plus"); )
const href = );
tagName === "a" ? `href="${assignedPath}" data-auto-route="true"` : "";
return `<${tagName} class="assigned-to discourse-tag simple" ${href}> const assignedTo = []
.concat(
assignedToUser,
assignedToGroup,
assignedToIndirectly ? Object.values(assignedToIndirectly) : []
)
.filter((element) => element)
.flat()
.uniqBy((assignee) => assignee.assign_path);
if (assignedTo) {
return assignedTo
.map((assignee) => {
const assignedPath = getURL(assignee.assign_path);
const icon = iconHTML(assignee.assign_icon);
const name = assignee.username || assignee.name;
const tagName = params.tagName || "a";
const href =
tagName === "a"
? `href="${assignedPath}" data-auto-route="true"`
: "";
return `<${tagName} class="assigned-to discourse-tag simple" ${href}>
${icon} ${icon}
<span>${assignedToUser || assignedToGroup}</span> <span>${name}</span>
</${tagName}>`; </${tagName}>`;
})
.join("");
} }
}); });
@ -299,19 +357,31 @@ function initialize(api) {
const topicId = topic.id; const topicId = topic.id;
if (data.topic_id === topicId) { if (data.topic_id === topicId) {
let post;
if (data.post_id) {
post = topic.postStream.posts.find((p) => p.id === data.post_id);
}
const target = post || topic;
if (data.assigned_type === "User") { if (data.assigned_type === "User") {
topic.set( target.set(
"assigned_to_user_id", "assigned_to_user_id",
data.type === "assigned" ? data.assigned_to.id : null data.type === "assigned" ? data.assigned_to.id : null
); );
topic.set("assigned_to_user", data.assigned_to); target.set("assigned_to_user", data.assigned_to);
} }
if (data.assigned_type === "Group") { if (data.assigned_type === "Group") {
topic.set( target.set(
"assigned_to_group_id", "assigned_to_group_id",
data.type === "assigned" ? data.assigned_to.id : null data.type === "assigned" ? data.assigned_to.id : null
); );
topic.set("assigned_to_group", data.assigned_to); target.set("assigned_to_group", data.assigned_to);
}
if (data.post_id) {
if (data.type === "unassigned") {
delete topic.indirectly_assigned_to[data.post_id];
}
this.appEvents.trigger("post-stream:refresh", { id: data.post_id });
} }
} }
this.appEvents.trigger("header:update-topic", topic); this.appEvents.trigger("header:update-topic", topic);
@ -330,20 +400,32 @@ function initialize(api) {
}); });
api.decorateWidget("post-contents:after-cooked", (dec) => { api.decorateWidget("post-contents:after-cooked", (dec) => {
if (dec.attrs.post_number === 1) { const postModel = dec.getModel();
const postModel = dec.getModel(); if (postModel) {
if (postModel) { let assignedToUser, assignedToGroup, postAssignment, href;
const assignedToUser = get(postModel, "topic.assigned_to_user"); if (dec.attrs.post_number === 1) {
const assignedToGroup = get(postModel, "topic.assigned_to_group"); assignedToUser = get(postModel, "topic.assigned_to_user");
if (assignedToUser || assignedToGroup) { assignedToGroup = get(postModel, "topic.assigned_to_group");
return dec.widget.attach("assigned-to", { } else {
assignedToUser, postAssignment = postModel.topic.indirectly_assigned_to?.[postModel.id];
assignedToGroup, if (postAssignment?.username) {
href: assignedToUser assignedToUser = postAssignment;
? get(postModel, "topic.assignedToUserPath")
: get(postModel, "topic.assignedToGroupPath"),
});
} }
if (postAssignment?.name) {
assignedToGroup = postAssignment;
}
}
if (assignedToUser || assignedToGroup) {
href = assignedToUser
? assignedToUserPath(assignedToUser)
: assignedToGroupPath(assignedToGroup);
}
if (href) {
return dec.widget.attach("assigned-to", {
assignedToUser,
assignedToGroup,
href,
});
} }
} }
}); });
@ -441,6 +523,10 @@ export default {
withPluginApi("0.12.2", (api) => { withPluginApi("0.12.2", (api) => {
api.addGroupPostSmallActionCode("assigned_group"); api.addGroupPostSmallActionCode("assigned_group");
api.addGroupPostSmallActionCode("unassigned_group"); api.addGroupPostSmallActionCode("unassigned_group");
api.addGroupPostSmallActionCode("assigned_to_post");
api.addGroupPostSmallActionCode("assigned_group_to_post");
api.addGroupPostSmallActionCode("unassigned_from_post");
api.addGroupPostSmallActionCode("unassigned_group_from_post");
}); });
withPluginApi("0.12.3", (api) => { withPluginApi("0.12.3", (api) => {
api.addUserSearchOption("assignableGroups"); api.addUserSearchOption("assignableGroups");

View File

@ -10,33 +10,55 @@ export default DropdownSelectBoxComponent.extend({
showFullTitle: true, showFullTitle: true,
computeContent() { computeContent() {
return [ let options = [];
{ if (this.assignee) {
id: "unassign", options = options.concat([
icon: this.group ? "group-times" : "user-times", {
name: I18n.t("discourse_assign.unassign.title"), id: "unassign",
description: I18n.t("discourse_assign.unassign.help", { icon: this.group ? "group-times" : "user-times",
username: this.assignee, name: I18n.t("discourse_assign.unassign.title"),
}), description: I18n.t("discourse_assign.unassign.help", {
}, username: this.assignee,
{ }),
id: "reassign", },
icon: "users", {
name: I18n.t("discourse_assign.reassign.title"), id: "reassign",
description: I18n.t("discourse_assign.reassign.help"), icon: "users",
}, name: I18n.t("discourse_assign.reassign.title"),
]; description: I18n.t("discourse_assign.reassign.help"),
},
]);
}
if (this.topic.indirectly_assigned_to) {
Object.entries(this.topic.indirectly_assigned_to).forEach((entry) => {
const [postId, assignee] = entry;
options = options.concat({
id: `unassign_post_${postId}`,
icon: assignee.username ? "user-times" : "group-times",
name: I18n.t("discourse_assign.unassign_post.title"),
description: I18n.t("discourse_assign.unassign_post.help", {
username: assignee.username || assignee.name,
}),
});
});
}
return options;
}, },
@action @action
onSelect(id) { onSelect(id) {
switch (id) { switch (id) {
case "unassign": case "unassign":
this.unassign(this.topic, this.assignee); this.unassign(this.topic.id);
break; break;
case "reassign": case "reassign":
this.reassign(this.topic, this.assignee); this.reassign(this.topic, this.assignee);
break; break;
} }
const postId = id.match(/unassign_post_(\d+)/)?.[1];
if (postId) {
this.unassign(postId, "Post");
}
}, },
}); });

View File

@ -3,22 +3,23 @@ import { ajax } from "discourse/lib/ajax";
import showModal from "discourse/lib/show-modal"; import showModal from "discourse/lib/show-modal";
export default Service.extend({ export default Service.extend({
unassign(topicId) { unassign(targetId, targetType = "Topic") {
return ajax("/assign/unassign", { return ajax("/assign/unassign", {
type: "PUT", type: "PUT",
data: { data: {
target_id: topicId, target_id: targetId,
target_type: "Topic", target_type: targetType,
}, },
}); });
}, },
assign(topic) { assign(target, targetType = "Topic") {
return showModal("assign-user", { return showModal("assign-user", {
model: { model: {
username: topic.get("assigned_to_user.username"), username: target.get("assigned_to_user.username"),
group_name: topic.get("assigned_to_group.name"), group_name: target.get("assigned_to_group.name"),
topic, target,
targetType,
}, },
}); });
}, },

View File

@ -54,7 +54,7 @@
unassign=unassign unassign=unassign
reassign=reassign reassign=reassign
}} }}
{{else}} {{else if topic.assigned_to_group}}
{{assign-actions-dropdown {{assign-actions-dropdown
topic=topic topic=topic
assignee=topic.assigned_to_group.name assignee=topic.assigned_to_group.name
@ -62,5 +62,10 @@
unassign=unassign unassign=unassign
reassign=reassign reassign=reassign
}} }}
{{else}}
{{assign-actions-dropdown
topic=topic
unassign=unassign
}}
{{/if}} {{/if}}
</td> </td>

View File

@ -28,13 +28,19 @@
unassign=unassign unassign=unassign
reassign=reassign reassign=reassign
}} }}
{{else}} {{else if topic.assigned_to_group}}
{{assign-actions-dropdown {{assign-actions-dropdown
topic=topic topic=topic
assignee=topic.assigned_to_group.name assignee=topic.assigned_to_group.name
group=true
unassign=unassign unassign=unassign
reassign=reassign reassign=reassign
}} }}
{{else}}
{{assign-actions-dropdown
topic=topic
unassign=unassign
}}
{{/if}} {{/if}}
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>

View File

@ -1,6 +1,6 @@
{{#d-modal-body title="discourse_assign.assign_modal.title" class="assign"}} {{#d-modal-body title=(concat "discourse_assign.assign" i18nSuffix ".title") class="assign"}}
<div> <div>
{{i18n "discourse_assign.assign_modal.description"}} <p>{{i18n (concat "discourse_assign.assign" i18nSuffix ".description")}}</p>
{{email-group-user-chooser {{email-group-user-chooser
autocomplete="off" autocomplete="off"
value=assigneeName value=assigneeName

View File

@ -69,6 +69,10 @@
margin-top: 5px; margin-top: 5px;
} }
.assign-user-modal p {
margin-top: 0;
}
.assign-suggestions { .assign-suggestions {
margin-top: 15px; margin-top: 15px;
img { img {

View File

@ -7,8 +7,12 @@ en:
action_codes: action_codes:
assigned: "assigned %{who} %{when}" assigned: "assigned %{who} %{when}"
assigned_group: "assigned %{who} %{when}" assigned_group: "assigned %{who} %{when}"
assigned_to_post: "assigned %{who} to post %{when}"
assigned_group_to_post: "assigned %{who} to post %{when}"
unassigned: "unassigned %{who} %{when}" unassigned: "unassigned %{who} %{when}"
unassigned_group: "unassigned %{who} %{when}" unassigned_group: "unassigned %{who} %{when}"
unassigned_from_post: "unassigned %{who} from post %{when}"
unassigned_group_from_post: "unassigned %{who} from post %{when}"
discourse_assign: discourse_assign:
add_unassigned_filter: "Add 'unassigned' filter to category" add_unassigned_filter: "Add 'unassigned' filter to category"
cant_act: "You cannot act on flags that have been assigned to other users" cant_act: "You cannot act on flags that have been assigned to other users"
@ -26,6 +30,11 @@ en:
assign: assign:
title: "Assign" title: "Assign"
help: "Assign Topic to User" help: "Assign Topic to User"
assign_post:
title: "Assign Post"
unassign_post:
title: "Unassign from Post"
help: "Unassign %{username} from Post"
reassign: reassign:
title: "Re-assign" title: "Re-assign"
help: "Re-assign Topic to a different user" help: "Re-assign Topic to a different user"
@ -33,6 +42,9 @@ en:
title: "Assign Topic" title: "Assign 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"
assign_post_modal:
title: "Assign Post"
description: "Enter the name of the user you'd like to assign this post"
claim: claim:
title: "claim" title: "claim"
help: "Assign topic to yourself" help: "Assign topic to yourself"

View File

@ -20,6 +20,7 @@ en:
already_claimed: "That topic has already been claimed." already_claimed: "That topic has already been claimed."
already_assigned: "Topic is already assigned to @%{username}" already_assigned: "Topic is already assigned to @%{username}"
too_many_assigns: "@%{username} has already reached the maximum number of assigned topics (%{max})." too_many_assigns: "@%{username} has already reached the maximum number of assigned topics (%{max})."
too_many_assigns_for_topic: "Limit of %{limit} assignments per topic has been reached."
forbidden_assign_to: "@%{username} can't be assigned since they don't belong to assigned allowed groups." forbidden_assign_to: "@%{username} can't be assigned since they don't belong to assigned allowed groups."
forbidden_assignee_not_pm_participant: "@%{username} can't be assigned because they don't have access to this PM. You can grant @%{username} access by inviting them to this PM." forbidden_assignee_not_pm_participant: "@%{username} can't be assigned because they don't have access to this PM. You can grant @%{username} access by inviting them to this PM."
forbidden_assignee_cant_see_topic: "@%{username} can't be assigned because they don't have access to this topic." forbidden_assignee_cant_see_topic: "@%{username} can't be assigned because they don't have access to this topic."

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class RemoveTopicIdIndex < ActiveRecord::Migration[6.1]
def change
remove_index :assignments, :topic_id
end
end

View File

@ -4,14 +4,15 @@ module Jobs
class AssignNotification < ::Jobs::Base class AssignNotification < ::Jobs::Base
def execute(args) def execute(args)
raise Discourse::InvalidParameters.new(:topic_id) if args[:topic_id].nil? raise Discourse::InvalidParameters.new(:topic_id) if args[:topic_id].nil?
raise Discourse::InvalidParameters.new(:post_id) if args[:post_id].nil?
raise Discourse::InvalidParameters.new(:assigned_to_id) if args[:assigned_to_id].nil? raise Discourse::InvalidParameters.new(:assigned_to_id) if args[:assigned_to_id].nil?
raise Discourse::InvalidParameters.new(:assigned_to_type) if args[:assigned_to_type].nil? raise Discourse::InvalidParameters.new(:assigned_to_type) if args[:assigned_to_type].nil?
raise Discourse::InvalidParameters.new(:assigned_by_id) if args[:assigned_by_id].nil? raise Discourse::InvalidParameters.new(:assigned_by_id) if args[:assigned_by_id].nil?
raise Discourse::InvalidParameters.new(:silent) if args[:silent].nil? raise Discourse::InvalidParameters.new(:silent) if args[:silent].nil?
topic = Topic.find(args[:topic_id]) topic = Topic.find(args[:topic_id])
post = Post.find(args[:post_id])
assigned_by = User.find(args[:assigned_by_id]) assigned_by = User.find(args[:assigned_by_id])
first_post = topic.posts.find_by(post_number: 1)
assigned_to = args[:assigned_to_type] == "User" ? User.find(args[:assigned_to_id]) : Group.find(args[:assigned_to_id]) assigned_to = args[:assigned_to_type] == "User" ? User.find(args[:assigned_to_id]) : Group.find(args[:assigned_to_id])
assigned_to_users = args[:assigned_to_type] == "User" ? [assigned_to] : assigned_to.users assigned_to_users = args[:assigned_to_type] == "User" ? [assigned_to] : assigned_to.users
@ -20,9 +21,9 @@ module Jobs
next if assigned_by == user next if assigned_by == user
PostAlerter.new(first_post).create_notification_alert( PostAlerter.new(post).create_notification_alert(
user: user, user: user,
post: first_post, post: post,
username: assigned_by.username, username: assigned_by.username,
notification_type: Notification.types[:custom], notification_type: Notification.types[:custom],
excerpt: I18n.t( excerpt: I18n.t(
@ -37,7 +38,7 @@ module Jobs
notification_type: Notification.types[:custom], notification_type: Notification.types[:custom],
user_id: user.id, user_id: user.id,
topic_id: topic.id, topic_id: topic.id,
post_number: 1, post_number: post.post_number,
high_priority: true, high_priority: true,
data: { data: {
message: args[:assigned_to_type] == "User" ? 'discourse_assign.assign_notification' : 'discourse_assign.assign_group_notification', message: args[:assigned_to_type] == "User" ? 'discourse_assign.assign_notification' : 'discourse_assign.assign_group_notification',

View File

@ -17,7 +17,6 @@ module Jobs
notification_type: Notification.types[:custom], notification_type: Notification.types[:custom],
user_id: user.id, user_id: user.id,
topic_id: topic.id, topic_id: topic.id,
post_number: 1
).where("data like '%discourse_assign.assign_notification%' OR data like '%discourse_assign.assign_group_notification%'").destroy_all ).where("data like '%discourse_assign.assign_notification%' OR data like '%discourse_assign.assign_group_notification%'").destroy_all
end end
end end

View File

@ -4,6 +4,8 @@ require 'email/sender'
require 'nokogiri' require 'nokogiri'
class ::Assigner class ::Assigner
ASSIGNMENTS_PER_TOPIC_LIMIT = 5
def self.backfill_auto_assign def self.backfill_auto_assign
staff_mention = User staff_mention = User
.assign_allowed .assign_allowed
@ -107,7 +109,6 @@ class ::Assigner
end end
def self.publish_topic_tracking_state(topic, user_id) def self.publish_topic_tracking_state(topic, user_id)
# TODO decide later if we want general or separate method to publish about post tracking
if topic.private_message? if topic.private_message?
MessageBus.publish( MessageBus.publish(
"/private-messages/assigned", "/private-messages/assigned",
@ -190,6 +191,8 @@ class ::Assigner
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 when topic.assignment&.assigned_to_id == assign_to.id
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
:too_many_assigns_for_topic
when !can_assign_to?(assign_to) when !can_assign_to?(assign_to)
:too_many_assigns :too_many_assigns
end end
@ -205,20 +208,20 @@ class ::Assigner
serializer = assignment.assigned_to_user? ? BasicUserSerializer : BasicGroupSerializer serializer = assignment.assigned_to_user? ? BasicUserSerializer : BasicGroupSerializer
#TODO assign notification for post
Jobs.enqueue(:assign_notification, Jobs.enqueue(:assign_notification,
topic_id: topic.id, topic_id: topic.id,
post_id: topic_target? ? first_post.id : @target.id,
assigned_to_id: assign_to.id, assigned_to_id: assign_to.id,
assigned_to_type: type, assigned_to_type: type,
assigned_by_id: @assigned_by.id, assigned_by_id: @assigned_by.id,
silent: silent) silent: silent)
#TODO message bus for post assignment
MessageBus.publish( MessageBus.publish(
"/staff/topic-assignment", "/staff/topic-assignment",
{ {
type: "assigned", type: "assigned",
topic_id: topic.id, topic_id: topic.id,
post_id: post_target? && @target.id,
assigned_type: type, assigned_type: type,
assigned_to: serializer.new(assign_to, scope: Guardian.new, root: false).as_json assigned_to: serializer.new(assign_to, scope: Guardian.new, root: false).as_json
}, },
@ -247,16 +250,14 @@ class ::Assigner
end end
end end
if !silent if !silent
#TODO moderator post when assigned to post
topic.add_moderator_post( topic.add_moderator_post(
@assigned_by, @assigned_by,
nil, nil,
bump: false, bump: false,
post_type: SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper], post_type: SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper],
action_code: assignment.assigned_to_user? ? "assigned" : "assigned_group", action_code: moderator_post_assign_action_code(assignment),
custom_fields: { "action_code_who" => assign_to.is_a?(User) ? assign_to.username : assign_to.name } custom_fields: { "action_code_who" => assign_to.is_a?(User) ? assign_to.username : assign_to.name }
) )
end end
# Create a webhook event # Create a webhook event
@ -296,7 +297,6 @@ class ::Assigner
first_post.publish_change_to_clients!(:revised, reload_topic: true) first_post.publish_change_to_clients!(:revised, reload_topic: true)
#TODO unassign notification for post
Jobs.enqueue(:unassign_notification, Jobs.enqueue(:unassign_notification,
topic_id: topic.id, topic_id: topic.id,
assigned_to_id: assignment.assigned_to.id, assigned_to_id: assignment.assigned_to.id,
@ -321,7 +321,6 @@ class ::Assigner
assigned_to = assignment.assigned_to assigned_to = assignment.assigned_to
# TODO unassign post for post assignment
if SiteSetting.unassign_creates_tracking_post && !silent if SiteSetting.unassign_creates_tracking_post && !silent
post_type = SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper] post_type = SiteSetting.assigns_public ? Post.types[:small_action] : Post.types[:whisper]
topic.add_moderator_post( topic.add_moderator_post(
@ -329,7 +328,7 @@ class ::Assigner
bump: false, bump: false,
post_type: post_type, post_type: post_type,
custom_fields: { "action_code_who" => assigned_to.is_a?(User) ? assigned_to.username : assigned_to.name }, custom_fields: { "action_code_who" => assigned_to.is_a?(User) ? assigned_to.username : assigned_to.name },
action_code: assignment.assigned_to_user? ? "unassigned" : "unassigned_group", action_code: moderator_post_unassign_action_code(assignment),
) )
end end
@ -362,9 +361,35 @@ class ::Assigner
{ {
type: 'unassigned', type: 'unassigned',
topic_id: topic.id, topic_id: topic.id,
post_id: post_target? && @target.id,
assigned_type: assignment.assigned_to.is_a?(User) ? "User" : "Group"
}, },
user_ids: allowed_user_ids user_ids: allowed_user_ids
) )
end end
end end
private
def moderator_post_assign_action_code(assignment)
suffix =
if assignment.target.is_a?(Post)
"_to_post"
elsif assignment.target.is_a?(Topic)
""
end
return "assigned#{suffix}" if assignment.assigned_to_user?
return "assigned_group#{suffix}" if assignment.assigned_to_group?
end
def moderator_post_unassign_action_code(assignment)
suffix =
if assignment.target.is_a?(Post)
"_from_post"
elsif assignment.target.is_a?(Topic)
""
end
return "unassigned#{suffix}" if assignment.assigned_to_user?
return "unassigned_group#{suffix}" if assignment.assigned_to_group?
end
end end

View File

@ -4,12 +4,12 @@ module DiscourseAssign
module Helpers module Helpers
def self.build_assigned_to_user(user, topic) def self.build_assigned_to_user(user, topic)
return if !user return if !user
{ {
username: user.username, username: user.username,
name: user.name, name: user.name,
avatar_template: user.avatar_template, avatar_template: user.avatar_template,
assigned_at: Assignment.where(target: topic).pluck_first(:created_at) assign_icon: 'user-plus',
assign_path: SiteSetting.assigns_user_url_path.gsub("{username}", user.username),
} }
end end
@ -22,8 +22,19 @@ module DiscourseAssign
flair_color: group.flair_color, flair_color: group.flair_color,
flair_icon: group.flair_icon, flair_icon: group.flair_icon,
flair_upload_id: group.flair_upload_id, flair_upload_id: group.flair_upload_id,
assigned_at: Assignment.where(target: topic).pluck_first(:created_at) assign_icon: 'group-plus',
assign_path: "/g/#{group.name}/assigned/everyone",
} }
end end
def self.build_indirectly_assigned_to(post_assignments, topic)
post_assignments.map do |post_id, assigned_to|
if (assigned_to.is_a?(User))
[post_id, build_assigned_to_user(assigned_to, topic)]
elsif assigned_to.is_a?(Group)
[post_id, build_assigned_to_group(assigned_to, topic)]
end
end.to_h
end
end end
end end

View File

@ -52,6 +52,10 @@ after_initialize do
has_one :assignment, as: :target, dependent: :destroy has_one :assignment, as: :target, dependent: :destroy
end end
class ::Post
has_one :assignment, as: :target, dependent: :destroy
end
class ::Group class ::Group
scope :assignable, ->(user) { scope :assignable, ->(user) {
where("assignable_level in (:levels) OR where("assignable_level in (:levels) OR
@ -195,6 +199,10 @@ after_initialize do
end end
end end
TopicView.on_preload do |topic_view|
topic_view.instance_variable_set(:@posts, topic_view.posts.includes(:assignment))
end
TopicList.on_preload do |topics, topic_list| TopicList.on_preload do |topics, topic_list|
if SiteSetting.assign_enabled? if SiteSetting.assign_enabled?
can_assign = topic_list.current_user && topic_list.current_user.can_assign? can_assign = topic_list.current_user && topic_list.current_user.can_assign?
@ -202,7 +210,7 @@ after_initialize do
if allowed_access && topics.length > 0 if allowed_access && topics.length > 0
assignments = Assignment.strict_loading.where(topic: topics) assignments = Assignment.strict_loading.where(topic: topics)
assignments_map = assignments.index_by(&:topic_id) assignments_map = assignments.group_by(&:topic_id)
user_ids = assignments.filter { |assignment| assignment.assigned_to_user? }.map(&:assigned_to_id) user_ids = assignments.filter { |assignment| assignment.assigned_to_user? }.map(&:assigned_to_id)
users_map = User.where(id: user_ids).select(UserLookup.lookup_columns).index_by(&:id) users_map = User.where(id: user_ids).select(UserLookup.lookup_columns).index_by(&:id)
@ -211,10 +219,23 @@ after_initialize do
groups_map = Group.where(id: group_ids).index_by(&:id) groups_map = Group.where(id: group_ids).index_by(&:id)
topics.each do |topic| topics.each do |topic|
assignment = assignments_map[topic.id] assignments = assignments_map[topic.id]
assigned_to = users_map[assignment.assigned_to_id] if assignment&.assigned_to_user? direct_assignment = assignments&.find { |assignment| assignment.target_type == "Topic" && assignment.target_id == topic.id }
assigned_to = groups_map[assignment.assigned_to_id] if assignment&.assigned_to_group? indirectly_assigned_to = {}
assignments&.each do |assignment|
next if assignment.target_type == "Topic"
next indirectly_assigned_to[assignment.target_id] = users_map[assignment.assigned_to_id] if assignment&.assigned_to_user?
next indirectly_assigned_to[assignment.target_id] = groups_map[assignment.assigned_to_id] if assignment&.assigned_to_group?
end&.compact&.uniq
assigned_to =
if direct_assignment&.assigned_to_user?
users_map[direct_assignment.assigned_to_id]
elsif direct_assignment&.assigned_to_group?
groups_map[direct_assignment.assigned_to_id]
end
topic.preload_assigned_to(assigned_to) topic.preload_assigned_to(assigned_to)
topic.preload_indirectly_assigned_to(indirectly_assigned_to)
end end
end end
end end
@ -389,10 +410,22 @@ after_initialize do
@assigned_to = assignment&.assigned_to @assigned_to = assignment&.assigned_to
end end
add_to_class(:topic, :indirectly_assigned_to) do
return @indirectly_assigned_to if defined?(@indirectly_assigned_to)
@indirectly_assigned_to = Assignment.where(topic_id: id, target_type: "Post").inject({}) do |acc, assignment|
acc[assignment.target_id] = assignment.assigned_to
acc
end
end
add_to_class(:topic, :preload_assigned_to) do |assigned_to| add_to_class(:topic, :preload_assigned_to) do |assigned_to|
@assigned_to = assigned_to @assigned_to = assigned_to
end end
add_to_class(:topic, :preload_indirectly_assigned_to) do |indirectly_assigned_to|
@indirectly_assigned_to = indirectly_assigned_to
end
# TopicList serializer # TopicList serializer
add_to_serializer(:topic_list, :assigned_messages_count) do add_to_serializer(:topic_list, :assigned_messages_count) do
TopicQuery.new(object.current_user, guardian: scope, limit: false) TopicQuery.new(object.current_user, guardian: scope, limit: false)
@ -426,6 +459,14 @@ after_initialize do
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to&.is_a?(Group) (SiteSetting.assigns_public || scope.can_assign?) && object.topic.assigned_to&.is_a?(Group)
end end
add_to_serializer(:topic_view, :indirectly_assigned_to) do
DiscourseAssign::Helpers.build_indirectly_assigned_to(object.topic.indirectly_assigned_to, object.topic)
end
add_to_serializer(:topic_view, :include_indirectly_assigned_to?) do
(SiteSetting.assigns_public || scope.can_assign?) && object.topic.indirectly_assigned_to.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)
@ -443,7 +484,23 @@ after_initialize do
(SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group) (SiteSetting.assigns_public || scope.can_assign?) && object.assigned_to&.is_a?(Group)
end end
add_to_serializer(:suggested_topic, :indirectly_assigned_to) do
DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object)
end
add_to_serializer(:suggested_topic, :include_indirectly_assigned_to?) do
(SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present?
end
# TopicListItem serializer # TopicListItem serializer
add_to_serializer(:topic_list_item, :indirectly_assigned_to) do
DiscourseAssign::Helpers.build_indirectly_assigned_to(object.indirectly_assigned_to, object)
end
add_to_serializer(:topic_list_item, :include_indirectly_assigned_to?) do
(SiteSetting.assigns_public || scope.can_assign?) && object.indirectly_assigned_to.present?
end
add_to_serializer(:topic_list_item, :assigned_to_user) do add_to_serializer(:topic_list_item, :assigned_to_user) do
BasicUserSerializer.new(object.assigned_to, scope: scope, root: false).as_json BasicUserSerializer.new(object.assigned_to, scope: scope, root: false).as_json
end end
@ -504,6 +561,22 @@ after_initialize do
topic.assigned_to topic.assigned_to
end end
add_to_serializer(:basic_user, :assign_icon) do
'user-plus'
end
add_to_serializer(:basic_user, :assign_path) do
return if !object.is_a?(User)
SiteSetting.assigns_user_url_path.gsub("{username}", object.username)
end
add_to_serializer(:basic_group, :assign_icon) do
'group-plus'
end
add_to_serializer(:basic_group, :assign_path) do
"/g/#{object.name}/assigned/everyone"
end
add_to_serializer(:user_bookmark, 'include_assigned_to_user?') do add_to_serializer(:user_bookmark, 'include_assigned_to_user?') do
(SiteSetting.assigns_public || scope.can_assign?) && topic.assigned_to&.is_a?(User) (SiteSetting.assigns_public || scope.can_assign?) && topic.assigned_to&.is_a?(User)
end end
@ -516,6 +589,23 @@ after_initialize do
(SiteSetting.assigns_public || scope.can_assign?) && topic.assigned_to&.is_a?(Group) (SiteSetting.assigns_public || scope.can_assign?) && topic.assigned_to&.is_a?(Group)
end end
# PostSerializer
add_to_serializer(:post, :assigned_to_user) do
object.assignment&.assigned_to
end
add_to_serializer(:post, 'include_assigned_to_user?') do
(SiteSetting.assigns_public || scope.can_assign?) && object.assignment&.assigned_to&.is_a?(User)
end
add_to_serializer(:post, :assigned_to_group, false) do
object.assignment&.assigned_to
end
add_to_serializer(:post, 'include_assigned_to_group?') do
(SiteSetting.assigns_public || scope.can_assign?) && object.assignment&.assigned_to&.is_a?(Group)
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

@ -29,7 +29,7 @@ RSpec.describe Jobs::AssignNotification do
it 'sends notification alert' do it 'sends notification alert' do
messages = MessageBus.track_publish("/notification-alert/#{user2.id}") do messages = MessageBus.track_publish("/notification-alert/#{user2.id}") do
described_class.new.execute({ topic_id: topic.id, assigned_to_id: user2.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: user2.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false })
end end
expect(messages.length).to eq(1) expect(messages.length).to eq(1)
@ -41,7 +41,7 @@ RSpec.describe Jobs::AssignNotification do
assign_allowed_group.add(user) assign_allowed_group.add(user)
assert_publish_topic_state(pm, user) do assert_publish_topic_state(pm, user) do
described_class.new.execute({ topic_id: pm.id, assigned_to_id: pm.allowed_users.first.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false }) described_class.new.execute({ topic_id: pm.id, post_id: pm_post.id, assigned_to_id: pm.allowed_users.first.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false })
end end
end end
@ -58,7 +58,7 @@ RSpec.describe Jobs::AssignNotification do
topic_title: topic.title topic_title: topic.title
}.to_json }.to_json
) )
described_class.new.execute({ topic_id: topic.id, assigned_to_id: user2.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: user2.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false })
end end
end end
@ -76,19 +76,19 @@ RSpec.describe Jobs::AssignNotification do
it 'sends notification alert to all group members' do it 'sends notification alert to all group members' do
messages = MessageBus.track_publish("/notification-alert/#{user2.id}") do messages = MessageBus.track_publish("/notification-alert/#{user2.id}") do
described_class.new.execute({ topic_id: topic.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false })
end end
expect(messages.length).to eq(1) expect(messages.length).to eq(1)
expect(messages.first.data[:excerpt]).to eq("assigned you the topic '#{topic.title}'") expect(messages.first.data[:excerpt]).to eq("assigned you the topic '#{topic.title}'")
messages = MessageBus.track_publish("/notification-alert/#{user3.id}") do messages = MessageBus.track_publish("/notification-alert/#{user3.id}") do
described_class.new.execute({ topic_id: topic.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false })
end end
expect(messages.length).to eq(1) expect(messages.length).to eq(1)
expect(messages.first.data[:excerpt]).to eq("assigned you the topic '#{topic.title}'") expect(messages.first.data[:excerpt]).to eq("assigned you the topic '#{topic.title}'")
messages = MessageBus.track_publish("/notification-alert/#{user4.id}") do messages = MessageBus.track_publish("/notification-alert/#{user4.id}") do
described_class.new.execute({ topic_id: topic.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false })
end end
expect(messages.length).to eq(0) expect(messages.length).to eq(0)
end end
@ -109,7 +109,7 @@ RSpec.describe Jobs::AssignNotification do
) )
end end
described_class.new.execute({ topic_id: topic.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false })
end end
end end
end end

View File

@ -27,10 +27,10 @@ RSpec.describe Jobs::UnassignNotification do
context 'User' do context 'User' do
it 'deletes notifications' do it 'deletes notifications' do
Jobs::AssignNotification.new.execute({ topic_id: topic.id, assigned_to_id: user2.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false }) Jobs::AssignNotification.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: user2.id, assigned_to_type: 'User', assigned_by_id: user1.id, silent: false })
expect { expect {
described_class.new.execute({ topic_id: topic.id, assigned_to_id: user2.id, assigned_to_type: 'User' }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: user2.id, assigned_to_type: 'User' })
}.to change { user2.notifications.count }.by(-1) }.to change { user2.notifications.count }.by(-1)
end end
@ -39,7 +39,7 @@ RSpec.describe Jobs::UnassignNotification do
assign_allowed_group.add(user) assign_allowed_group.add(user)
assert_publish_topic_state(pm, user) do assert_publish_topic_state(pm, user) do
described_class.new.execute({ topic_id: pm.id, assigned_to_id: pm.allowed_users.first.id, assigned_to_type: 'User' }) described_class.new.execute({ topic_id: pm.id, post_id: pm_post.id, assigned_to_id: pm.allowed_users.first.id, assigned_to_type: 'User' })
end end
end end
end end
@ -55,10 +55,10 @@ RSpec.describe Jobs::UnassignNotification do
end end
it 'deletes notifications' do it 'deletes notifications' do
Jobs::AssignNotification.new.execute({ topic_id: topic.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false }) Jobs::AssignNotification.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: group.id, assigned_to_type: 'Group', assigned_by_id: user1.id, silent: false })
expect { expect {
described_class.new.execute({ topic_id: topic.id, assigned_to_id: group.id, assigned_to_type: 'Group' }) described_class.new.execute({ topic_id: topic.id, post_id: post.id, assigned_to_id: group.id, assigned_to_type: 'Group' })
}.to change { Notification.count }.by(-2) }.to change { Notification.count }.by(-2)
end end
end end

View File

@ -45,8 +45,9 @@ describe FlaggedTopicSerializer do
expect(json[:flagged_topic][:assigned_to_user]).to match({ expect(json[:flagged_topic][:assigned_to_user]).to match({
username: user.username, username: user.username,
name: user.name, name: user.name,
assign_icon: "user-plus",
avatar_template: /letter_avatar_proxy.*/, avatar_template: /letter_avatar_proxy.*/,
assigned_at: Assignment.last.created_at, assign_path: "/u/#{user.username}/activity/assigned",
}) })
expect(json[:flagged_topic]).to_not have_key(:assigned_to_group) expect(json[:flagged_topic]).to_not have_key(:assigned_to_group)
end end
@ -75,7 +76,8 @@ describe FlaggedTopicSerializer do
flair_color: assign_allowed_group.flair_color, flair_color: assign_allowed_group.flair_color,
flair_icon: assign_allowed_group.flair_icon, flair_icon: assign_allowed_group.flair_icon,
flair_upload_id: assign_allowed_group.flair_upload_id, flair_upload_id: assign_allowed_group.flair_upload_id,
assigned_at: Assignment.last.created_at, assign_icon: "group-plus",
assign_path: "/g/#{assign_allowed_group.name}/assigned/everyone",
}) })
expect(json[:flagged_topic]).to_not have_key(:assigned_to_user) expect(json[:flagged_topic]).to_not have_key(:assigned_to_user)
end end

View File

@ -80,6 +80,22 @@ acceptance("Discourse Assign | Assign desktop", function (needs) {
return helper.response({ success: true }); return helper.response({ success: true });
}); });
}); });
test("Post contains hidden assign button", async (assert) => {
updateCurrentUser({ can_assign: true });
await visit("/t/internationalization-localization/280");
assert.ok(
!exists("#post_2 .extra-buttons .d-icon-user-plus"),
"assign to post button is hidden"
);
await click("#post_2 button.show-more-actions");
assert.ok(
exists("#post_2 .extra-buttons .d-icon-user-plus"),
"assign to post button exists"
);
await click("#post_2 .extra-buttons .d-icon-user-plus");
assert.ok(exists(".assign.modal-body"), "assign modal opens");
});
test("Footer dropdown contains button", async (assert) => { test("Footer dropdown contains button", async (assert) => {
updateCurrentUser({ can_assign: true }); updateCurrentUser({ can_assign: true });

View File

@ -25,7 +25,6 @@ acceptance("Discourse Assign | Assigned topic", function (needs) {
name: "Robin Ward", name: "Robin Ward",
avatar_template: avatar_template:
"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png", "/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
assigned_at: "2021-06-13T16:33:14.189Z",
}; };
return helper.response(topic); return helper.response(topic);
}); });
@ -34,7 +33,6 @@ acceptance("Discourse Assign | Assigned topic", function (needs) {
let topic = cloneJSON(topicFixtures["/t/28830/1.json"]); let topic = cloneJSON(topicFixtures["/t/28830/1.json"]);
topic["assigned_to_group"] = { topic["assigned_to_group"] = {
name: "Developers", name: "Developers",
assigned_at: "2021-06-13T16:33:14.189Z",
}; };
return helper.response(topic); return helper.response(topic);
}); });