FEATURE: Assignments summary tab for groups (#70)

This commit is contained in:
Ahmed Gagan 2020-07-10 14:15:18 +05:30 committed by GitHub
parent f5e53f8d22
commit ae83f70e21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 470 additions and 7 deletions

View File

@ -0,0 +1,8 @@
export default {
resource: "group",
map() {
this.route("assignments", function() {
this.route("show", { path: "/:filter" });
});
}
};

View File

@ -0,0 +1 @@
{{group-assignments-menu-item group = group}}

View File

@ -0,0 +1,18 @@
import UserTopicsList from "discourse/controllers/user-topics-list";
export default UserTopicsList.extend({
user: Ember.inject.controller(),
taskActions: Ember.inject.service(),
actions: {
unassign(topic) {
this.taskActions
.unassign(topic.get("id"))
.then(() => this.send("changeAssigned"));
},
reassign(topic) {
const controller = this.taskActions.assign(topic);
controller.set("model.onSuccess", () => this.send("changeAssigned"));
}
}
});

View File

@ -0,0 +1,34 @@
import Controller, { inject as controller } from "@ember/controller";
export default Controller.extend({
application: controller(),
loading: false,
findMembers(refresh) {
if (this.loading || !this.model) {
return;
}
if (!refresh && this.model.members.length >= this.model.user_count) {
this.set("application.showFooter", true);
return;
}
this.set("loading", true);
this.model
.findMembers({ order: "", asc: true, filter: null }, refresh)
.finally(() => {
this.setProperties({
"application.showFooter":
this.model.members.length >= this.model.user_count,
loading: false
});
});
},
actions: {
loadMore: function() {
this.findMembers();
}
}
});

View File

@ -0,0 +1,27 @@
import DiscourseRoute from "discourse/routes/discourse";
export default DiscourseRoute.extend({
model(params) {
let filter = null;
if (params.filter !== "everyone") {
filter = `topics/messages-assigned/${params.filter}`;
} else {
filter = `topics/group-topics-assigned/${this.modelFor("group").get(
"name"
)}`;
}
return this.store.findFiltered("topicList", {
filter: filter
});
},
renderTemplate() {
this.render("group-topics-list");
},
actions: {
changeAssigned() {
this.refresh();
}
}
});

View File

@ -0,0 +1,24 @@
import Route from "@ember/routing/route";
export default Route.extend({
model() {
return this.modelFor("group");
},
setupController(controller, model) {
controller.setProperties({
model,
showing: "members"
});
controller.findMembers(true);
},
redirect(model, transition) {
if (transition.to.params.hasOwnProperty("filter")) {
this.transitionTo("group.assignments.show", transition.to.params.filter);
} else {
this.transitionTo("group.assignments.show", "everyone");
}
}
});

View File

@ -0,0 +1,5 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "li"
});

View File

@ -0,0 +1,8 @@
export default Ember.Component.extend({
canAssign: false,
init() {
this._super(...arguments);
this.set("canAssign", this.currentUser.can_assign);
}
});

View File

@ -0,0 +1,9 @@
{{#if show-avatar}}
{{#link-to "group.assignments.show" filter.username_lower}}
{{avatar filter avatarTemplatePath="avatar_template" usernamePath="username" imageSize="small"}} {{filter.username}}
{{/link-to}}
{{else}}
{{#link-to "group.assignments.show" filter}}
{{i18n 'discourse_assign.group_everyone'}}
{{/link-to}}
{{/if}}

View File

@ -0,0 +1,9 @@
{{#if canAssign}}
<ul class ='nav-pills'>
<li>
{{#link-to 'group.assignments' group}}
{{d-icon "user-plus" class="glyph"}}{{i18n 'discourse_assign.group_assignments'}}{{concat ' (' group.assignment_count concat ')'}}
{{/link-to}}
</li>
</ul>
{{/if}}

View File

@ -0,0 +1,18 @@
{{#load-more class="paginated-topics-list" selector=".paginated-topics-list .topic-list tr" action=(action "loadMore")}}
{{basic-assigned-topic-list topicList=model
hideCategory=hideCategory
showPosters=showPosters
bulkSelectEnabled=bulkSelectEnabled
selected=selected
hasIncoming=hasIncoming
incomingCount=incomingCount
showInserted=(action "showInserted")
tagsForUser=tagsForUser
unassign=(action 'unassign')
reassign=(action 'reassign')}}
{{conditional-loading-spinner condition=model.loadingMore}}
{{/load-more}}

View File

@ -0,0 +1,15 @@
{{log model}}
<section class="user-secondary-navigation">
{{#mobile-nav class="activity-nav" desktopClass="action-list activity-list nav-stacked" currentPath=router._router.currentPath}}
{{#load-more selector=".activity-nav li" action=(action "loadMore")}}
{{group-assignments-filter show-avatar=false filter="everyone" routeType=route_type}}
{{#each model.members as |member|}}
{{group-assignments-filter show-avatar=true filter=member routeType=route_type}}
{{/each}}
{{conditional-loading-spinner condition=loading}}
{{/load-more}}
{{/mobile-nav}}
</section>
<section class="user-content">
{{outlet}}
</section>

View File

@ -12,6 +12,8 @@ en:
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"
cant_act_unclaimed: "You must claim this topic before acting on flags." cant_act_unclaimed: "You must claim this topic before acting on flags."
assigned: "Assigned" assigned: "Assigned"
group_assignments: "Assignments"
group_everyone: "Everyone"
assigned_to: "Assigned to" assigned_to: "Assigned to"
assign_notification: "<p><span>%{username}</span> %{description}</p>" assign_notification: "<p><span>%{username}</span> %{description}</p>"
unassign: unassign:

View File

@ -20,7 +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})."
forbidden_assign_to: "@%{username} can't be assigned since they don't have access to this topic." forbidden_assign_to: "@%{username} can't be assigned since they don't belong to assigned allowed groups."
flag_assigned: "Sorry, that flag's topic is assigned to another user" flag_assigned: "Sorry, that flag's topic is assigned to another user"
flag_unclaimed: "You must claim that topic before acting on the flag" flag_unclaimed: "You must claim that topic before acting on the flag"
topic_assigned_excerpt: "assigned you the topic '%{title}'" topic_assigned_excerpt: "assigned you the topic '%{title}'"

View File

@ -20,7 +20,10 @@ load File.expand_path('../lib/discourse_assign/helpers.rb', __FILE__)
Discourse::Application.routes.append do Discourse::Application.routes.append do
mount ::DiscourseAssign::Engine, at: "/assign" mount ::DiscourseAssign::Engine, at: "/assign"
get "topics/private-messages-assigned/:username" => "list#private_messages_assigned", as: "topics_private_messages_assigned", constraints: { username: ::RouteFormat.username } get "topics/private-messages-assigned/:username" => "list#private_messages_assigned", as: "topics_private_messages_assigned", constraints: { username: ::RouteFormat.username }
get "topics/messages-assigned/:username" => "list#messages_assigned", as: "topics_messages_assigned", constraints: { username: ::RouteFormat.username } get "/topics/messages-assigned/:username" => "list#messages_assigned", constraints: { username: ::RouteFormat.username }
get "/topics/group-topics-assigned/:groupname" => "list#group_topics_assigned", constraints: { username: ::RouteFormat.username }
get "/g/:id/assignments" => "groups#index"
get "/g/:id/assignments/:route_type" => "groups#index"
end end
after_initialize do after_initialize do
@ -36,6 +39,16 @@ after_initialize do
add_to_serializer(:user, :reminders_frequency) do add_to_serializer(:user, :reminders_frequency) do
RemindAssignsFrequencySiteSettings.values RemindAssignsFrequencySiteSettings.values
end end
add_to_serializer(:group_show, :assignment_count) do
Topic.joins("JOIN topic_custom_fields tcf ON topics.id = tcf.topic_id AND tcf.name = 'assigned_to_id' AND tcf.value IS NOT NULL")
.where("tcf.value IN (SELECT group_users.user_id::varchar(255) FROM group_users WHERE (group_id IN (SELECT id FROM groups WHERE name = ?)))", object.name).count
end
add_to_serializer(:group_show, 'include_assignment_count?') do
scope.can_assign?
end
add_model_callback(UserCustomField, :before_save) do add_model_callback(UserCustomField, :before_save) do
self.value = self.value.to_i if self.name == frequency_field self.value = self.value.to_i if self.name == frequency_field
end end
@ -186,19 +199,66 @@ after_initialize do
require_dependency 'list_controller' require_dependency 'list_controller'
class ::ListController class ::ListController
generate_message_route(:private_messages_assigned) generate_message_route(:private_messages_assigned)
generate_message_route(:messages_assigned)
end end
add_to_class(:topic_query, :list_messages_assigned) do |user| add_to_class(:topic_query, :list_messages_assigned) do |user|
secure = Topic.listable_topics.secured(@guardian).or(Topic.private_messages_for_user(@user))
list = joined_topic_user.where(" list = joined_topic_user.where("
topics.id IN ( topics.id IN (
SELECT topic_id FROM topic_custom_fields SELECT topic_id FROM topic_custom_fields
WHERE name = 'assigned_to_id' WHERE name = 'assigned_to_id'
AND value = ?) AND value = ?)
", user.id.to_s) ", user.id.to_s)
.limit(per_page_setting)
.offset(per_page_setting * options[:page])
.order("topics.bumped_at DESC") .order("topics.bumped_at DESC")
list = list.merge(secure)
create_list(:assigned, {}, list) create_list(:assigned, { unordered: true }, list)
end
add_to_class(:list_controller, :messages_assigned) do
page = (params[:page].to_i || 0).to_i
user = User.find_by_username(params[:username])
raise Discourse::NotFound unless user
raise Discourse::InvalidAccess unless current_user.can_assign?
list_opts = build_topic_list_options
list_opts[:page] = page
list = generate_list_for("messages_assigned", user, list_opts)
list.more_topics_url = "/topics/messages-assigned/#{params[:username]}.json?page=#{page + 1}"
respond_with_list(list)
end
add_to_class(:topic_query, :list_group_topics_assigned) do |group|
secure = Topic.listable_topics.secured(@guardian).or(Topic.private_messages_for_user(@user))
list = joined_topic_user.where("
topics.id IN (
SELECT topic_id FROM topic_custom_fields
WHERE name = 'assigned_to_id'
AND value IN (SELECT user_id::varchar(255) from group_users where group_id = ?))
", group.id.to_s)
.limit(per_page_setting)
.offset(per_page_setting * options[:page])
.order("topics.bumped_at DESC")
list = list.merge(secure)
create_list(:assigned, { unordered: true }, list)
end
add_to_class(:list_controller, :group_topics_assigned) do
page = (params[:page].to_i || 0).to_i
group = Group.find_by("LOWER(name) = ?", params[:groupname])
raise Discourse::NotFound unless group
raise Discourse::InvalidAccess unless current_user.can_assign?
list_opts = build_topic_list_options
list_opts[:page] = page
list = generate_list_for("group_topics_assigned", group, list_opts)
list.more_topics_url = "/topics/group-topics-assigned/#{params[:groupname]}.json?page=#{page + 1}"
respond_with_list(list)
end end
add_to_class(:topic_query, :list_private_messages_assigned) do |user| add_to_class(:topic_query, :list_private_messages_assigned) do |user|

View File

@ -28,13 +28,13 @@ describe TopicQuery do
end end
it 'Includes topics and PMs assigned to user' do it 'Includes topics and PMs assigned to user' do
assigned_messages = TopicQuery.new(user).list_messages_assigned(user).topics assigned_messages = TopicQuery.new(user, { page: 0 }).list_messages_assigned(user).topics
expect(assigned_messages).to contain_exactly(@private_message, @topic) expect(assigned_messages).to contain_exactly(@private_message, @topic)
end end
it 'Excludes topics and PMs not assigned to user' do it 'Excludes topics and PMs not assigned to user' do
assigned_messages = TopicQuery.new(user2).list_messages_assigned(user2).topics assigned_messages = TopicQuery.new(user2, { page: 0 }).list_messages_assigned(user2).topics
expect(assigned_messages).to be_empty expect(assigned_messages).to be_empty
end end
@ -42,7 +42,32 @@ describe TopicQuery do
it 'Returns the results ordered by the bumped_at field' do it 'Returns the results ordered by the bumped_at field' do
@topic.update(bumped_at: 2.weeks.ago) @topic.update(bumped_at: 2.weeks.ago)
assigned_messages = TopicQuery.new(user).list_messages_assigned(user).topics assigned_messages = TopicQuery.new(user, { page: 0 }).list_messages_assigned(user).topics
expect(assigned_messages).to eq([@private_message, @topic])
end
end
describe '#list_group_topics_assigned' do
before do
@private_message = Fabricate(:private_message_topic, user: user)
@topic = Fabricate(:topic, user: user)
assign_to(@private_message, user)
assign_to(@topic, user2)
end
it 'Includes topics and PMs assigned to user' do
assigned_messages = TopicQuery.new(user, { page: 0 }).list_group_topics_assigned(assign_allowed_group).topics
expect(assigned_messages).to contain_exactly(@private_message, @topic)
end
it 'Returns the results ordered by the bumped_at field' do
@topic.update(bumped_at: 2.weeks.ago)
assigned_messages = TopicQuery.new(user, { page: 0 }).list_group_topics_assigned(assign_allowed_group).topics
expect(assigned_messages).to eq([@private_message, @topic]) expect(assigned_messages).to eq([@private_message, @topic])
end end

View File

@ -8,4 +8,8 @@ shared_context 'A group that is allowed to assign' do
def add_to_assign_allowed_group(user) def add_to_assign_allowed_group(user)
assign_allowed_group.add(user) assign_allowed_group.add(user)
end end
def get_assigned_allowed_group()
assign_allowed_group
end
end end

View File

@ -0,0 +1,26 @@
import { acceptance } from "helpers/qunit-helpers";
import { default as AssignedTopics } from "../fixtures/assigned-group-assignments-fixtures";
acceptance("GroupAssignments", {
loggedIn: true,
settings: { assign_enabled: true, assigns_user_url_path: "/" },
pretend(server, helper) {
const groupPath = "/topics/group-topics-assigned/discourse.json";
const memberPath = "/topics/messages-assigned/awesomerobot.json";
const groupAssigns = AssignedTopics[groupPath];
const memberAssigns = AssignedTopics[memberPath];
server.get(groupPath, () => helper.response(groupAssigns));
server.get(memberPath, () => helper.response(memberAssigns));
}
});
QUnit.test("Group Assignments Everyone", async assert => {
await visit("/g/discourse/assignments");
assert.equal(currentPath(), "group.assignments.show");
assert.ok(find(".topic-list-item").length === 1);
});
QUnit.test("Group Assignments Awesomerobot", async assert => {
await visit("/g/discourse/assignments/awesomerobot");
assert.equal(currentPath(), "group.assignments.show");
assert.ok(find(".topic-list-item").length === 1);
});

View File

@ -0,0 +1,170 @@
export default {
"/topics/group-topics-assigned/discourse.json": {
users: [
{
id: -2,
username: "discobot",
name: "discobot",
avatar_template: "/user_avatar/localhost/discobot/{size}/1_2.png"
},
{
id: -1,
username: "system",
name: "system",
avatar_template: "/user_avatar/localhost/system/{size}/2_2.png"
}
],
primary_groups: [],
topic_list: {
can_create_topic: true,
draft: null,
draft_key: "new_topic",
draft_sequence: 0,
per_page: 30,
topics: [
{
id: 10,
title: "Greetings!",
fancy_title: "Greetings!",
slug: "greetings",
posts_count: 1,
reply_count: 0,
highest_post_number: 4,
image_url:
"//localhost:3000/plugins/discourse-narrative-bot/images/font-awesome-ellipsis.png",
created_at: "2019-05-08T13:52:39.394Z",
last_posted_at: "2019-05-08T13:52:39.841Z",
bumped: true,
bumped_at: "2019-05-08T13:52:39.841Z",
unseen: false,
last_read_post_number: 4,
unread: 0,
new_posts: 0,
pinned: false,
unpinned: null,
visible: true,
closed: false,
archived: false,
notification_level: 3,
bookmarked: false,
liked: false,
views: 0,
like_count: 0,
has_summary: false,
archetype: "private_message",
last_poster_username: "discobot",
category_id: null,
pinned_globally: false,
featured_link: null,
posters: [
{
extras: "latest single",
description: "Original Poster, Most Recent Poster",
user_id: -2,
primary_group_id: null
}
],
participants: [
{
extras: "latest",
description: null,
user_id: -2,
primary_group_id: null
}
],
assigned_to_user: {
id: 19,
username: "eviltrout",
name: null,
avatar_template:
"/letter_avatar_proxy/v4/letter/r/ed8c4c/{size}.png"
}
}
]
}
},
"/topics/messages-assigned/awesomerobot.json": {
users: [
{
id: -2,
username: "discobot",
name: "discobot",
avatar_template: "/user_avatar/localhost/discobot/{size}/1_2.png"
},
{
id: -1,
username: "system",
name: "system",
avatar_template: "/user_avatar/localhost/system/{size}/2_2.png"
}
],
primary_groups: [],
topic_list: {
can_create_topic: true,
draft: null,
draft_key: "new_topic",
draft_sequence: 0,
per_page: 30,
topics: [
{
id: 10,
title: "Greetings!",
fancy_title: "Greetings!",
slug: "greetings",
posts_count: 1,
reply_count: 0,
highest_post_number: 4,
image_url:
"//localhost:3000/plugins/discourse-narrative-bot/images/font-awesome-ellipsis.png",
created_at: "2019-05-08T13:52:39.394Z",
last_posted_at: "2019-05-08T13:52:39.841Z",
bumped: true,
bumped_at: "2019-05-08T13:52:39.841Z",
unseen: false,
last_read_post_number: 4,
unread: 0,
new_posts: 0,
pinned: false,
unpinned: null,
visible: true,
closed: false,
archived: false,
notification_level: 3,
bookmarked: false,
liked: false,
views: 0,
like_count: 0,
has_summary: false,
archetype: "private_message",
last_poster_username: "discobot",
category_id: null,
pinned_globally: false,
featured_link: null,
posters: [
{
extras: "latest single",
description: "Original Poster, Most Recent Poster",
user_id: -2,
primary_group_id: null
}
],
participants: [
{
extras: "latest",
description: null,
user_id: -2,
primary_group_id: null
}
],
assigned_to_user: {
id: 2770,
username: "awesomerobot",
name: null,
avatar_template:
"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png"
}
}
]
}
}
};