diff --git a/assets/javascripts/discourse-assign/assigned-group-route-map.js.es6 b/assets/javascripts/discourse-assign/assigned-group-route-map.js.es6 new file mode 100644 index 0000000..7080da8 --- /dev/null +++ b/assets/javascripts/discourse-assign/assigned-group-route-map.js.es6 @@ -0,0 +1,8 @@ +export default { + resource: "group", + map() { + this.route("assignments", function() { + this.route("show", { path: "/:filter" }); + }); + } +}; diff --git a/assets/javascripts/discourse-assign/connectors/group-reports-nav-item/assigned-topic-list.hbs b/assets/javascripts/discourse-assign/connectors/group-reports-nav-item/assigned-topic-list.hbs new file mode 100644 index 0000000..bdae640 --- /dev/null +++ b/assets/javascripts/discourse-assign/connectors/group-reports-nav-item/assigned-topic-list.hbs @@ -0,0 +1 @@ +{{group-assignments-menu-item group = group}} \ No newline at end of file diff --git a/assets/javascripts/discourse-assign/controllers/group-assignments-show.js.es6 b/assets/javascripts/discourse-assign/controllers/group-assignments-show.js.es6 new file mode 100644 index 0000000..7ca8c5f --- /dev/null +++ b/assets/javascripts/discourse-assign/controllers/group-assignments-show.js.es6 @@ -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")); + } + } +}); diff --git a/assets/javascripts/discourse-assign/controllers/group-assignments.js.es6 b/assets/javascripts/discourse-assign/controllers/group-assignments.js.es6 new file mode 100644 index 0000000..a328734 --- /dev/null +++ b/assets/javascripts/discourse-assign/controllers/group-assignments.js.es6 @@ -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(); + } + } +}); diff --git a/assets/javascripts/discourse-assign/routes/group-assignments-show.js.es6 b/assets/javascripts/discourse-assign/routes/group-assignments-show.js.es6 new file mode 100644 index 0000000..c1d8f1e --- /dev/null +++ b/assets/javascripts/discourse-assign/routes/group-assignments-show.js.es6 @@ -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(); + } + } +}); diff --git a/assets/javascripts/discourse-assign/routes/group-assignments.js.es6 b/assets/javascripts/discourse-assign/routes/group-assignments.js.es6 new file mode 100644 index 0000000..c70a634 --- /dev/null +++ b/assets/javascripts/discourse-assign/routes/group-assignments.js.es6 @@ -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"); + } + } +}); diff --git a/assets/javascripts/discourse/components/group-assignments-filter.js.es6 b/assets/javascripts/discourse/components/group-assignments-filter.js.es6 new file mode 100644 index 0000000..e0188e1 --- /dev/null +++ b/assets/javascripts/discourse/components/group-assignments-filter.js.es6 @@ -0,0 +1,5 @@ +import Component from "@ember/component"; + +export default Component.extend({ + tagName: "li" +}); diff --git a/assets/javascripts/discourse/components/group-assignments-menu-item.js.es6 b/assets/javascripts/discourse/components/group-assignments-menu-item.js.es6 new file mode 100644 index 0000000..0c46d45 --- /dev/null +++ b/assets/javascripts/discourse/components/group-assignments-menu-item.js.es6 @@ -0,0 +1,8 @@ +export default Ember.Component.extend({ + canAssign: false, + + init() { + this._super(...arguments); + this.set("canAssign", this.currentUser.can_assign); + } +}); diff --git a/assets/javascripts/discourse/templates/components/group-assignments-filter.hbs b/assets/javascripts/discourse/templates/components/group-assignments-filter.hbs new file mode 100644 index 0000000..fb1e704 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/group-assignments-filter.hbs @@ -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}} diff --git a/assets/javascripts/discourse/templates/components/group-assignments-menu-item.hbs b/assets/javascripts/discourse/templates/components/group-assignments-menu-item.hbs new file mode 100644 index 0000000..0d1b27e --- /dev/null +++ b/assets/javascripts/discourse/templates/components/group-assignments-menu-item.hbs @@ -0,0 +1,9 @@ +{{#if canAssign}} + +{{/if}} diff --git a/assets/javascripts/discourse/templates/group-topics-list.hbs b/assets/javascripts/discourse/templates/group-topics-list.hbs new file mode 100644 index 0000000..8d177d2 --- /dev/null +++ b/assets/javascripts/discourse/templates/group-topics-list.hbs @@ -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}} + + + diff --git a/assets/javascripts/discourse/templates/group/assignments.hbs b/assets/javascripts/discourse/templates/group/assignments.hbs new file mode 100644 index 0000000..288e066 --- /dev/null +++ b/assets/javascripts/discourse/templates/group/assignments.hbs @@ -0,0 +1,15 @@ +{{log model}} +
+ {{#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}} +
+
+ {{outlet}} +
\ No newline at end of file diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e399288..8fdad2c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -12,6 +12,8 @@ en: 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." assigned: "Assigned" + group_assignments: "Assignments" + group_everyone: "Everyone" assigned_to: "Assigned to" assign_notification: "

%{username} %{description}

" unassign: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 977c98f..beec70c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -20,7 +20,7 @@ en: already_claimed: "That topic has already been claimed." already_assigned: 'Topic is already assigned to @%{username}' 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_unclaimed: "You must claim that topic before acting on the flag" topic_assigned_excerpt: "assigned you the topic '%{title}'" diff --git a/plugin.rb b/plugin.rb index 1537235..e219d11 100644 --- a/plugin.rb +++ b/plugin.rb @@ -20,7 +20,10 @@ 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: ::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 after_initialize do @@ -36,6 +39,16 @@ after_initialize do add_to_serializer(:user, :reminders_frequency) do RemindAssignsFrequencySiteSettings.values 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 self.value = self.value.to_i if self.name == frequency_field end @@ -186,19 +199,66 @@ after_initialize do require_dependency 'list_controller' class ::ListController generate_message_route(:private_messages_assigned) - generate_message_route(:messages_assigned) end 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(" topics.id IN ( SELECT topic_id FROM topic_custom_fields WHERE name = 'assigned_to_id' AND value = ?) ", user.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, {}, 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 add_to_class(:topic_query, :list_private_messages_assigned) do |user| diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index a7ae070..09519e0 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -28,13 +28,13 @@ describe TopicQuery do end 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) end 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 end @@ -42,7 +42,32 @@ describe TopicQuery do it 'Returns the results ordered by the bumped_at field' do @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]) end diff --git a/spec/support/assign_allowed_group.rb b/spec/support/assign_allowed_group.rb index a3c97fa..4623e41 100644 --- a/spec/support/assign_allowed_group.rb +++ b/spec/support/assign_allowed_group.rb @@ -8,4 +8,8 @@ shared_context 'A group that is allowed to assign' do def add_to_assign_allowed_group(user) assign_allowed_group.add(user) end + + def get_assigned_allowed_group() + assign_allowed_group + end end diff --git a/test/javascripts/acceptance/group-assignments-test.js.es6 b/test/javascripts/acceptance/group-assignments-test.js.es6 new file mode 100644 index 0000000..24cc23b --- /dev/null +++ b/test/javascripts/acceptance/group-assignments-test.js.es6 @@ -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); +}); diff --git a/test/javascripts/fixtures/assigned-group-assignments-fixtures.js.es6 b/test/javascripts/fixtures/assigned-group-assignments-fixtures.js.es6 new file mode 100644 index 0000000..72e3a39 --- /dev/null +++ b/test/javascripts/fixtures/assigned-group-assignments-fixtures.js.es6 @@ -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" + } + } + ] + } + } +};