FEATURE: Per-member counts in group assignment summary (#80)
This commit is contained in:
parent
4d5169c445
commit
816f3c0e97
|
@ -124,6 +124,30 @@ module DiscourseAssign
|
||||||
render json: { topics: serialize_data(topics, AssignedTopicSerializer) }
|
render json: { topics: serialize_data(topics, AssignedTopicSerializer) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def group_members
|
||||||
|
limit = (params[:limit] || 50).to_i
|
||||||
|
offset = params[:offset].to_i
|
||||||
|
|
||||||
|
raise Discourse::InvalidParameters.new(:limit) if limit < 0 || limit > 1000
|
||||||
|
raise Discourse::InvalidParameters.new(:offset) if offset < 0
|
||||||
|
raise Discourse::NotFound.new if !params[:group_name].present?
|
||||||
|
|
||||||
|
group = Group.find_by(name: params[:group_name])
|
||||||
|
|
||||||
|
guardian.ensure_can_see_group_members!(group)
|
||||||
|
|
||||||
|
members = User
|
||||||
|
.joins("LEFT OUTER JOIN group_users g on users.id=g.user_id LEFT OUTER JOIN user_options uo on uo.user_id=users.id LEFT OUTER JOIN topic_custom_fields tcf ON tcf.value::int = users.id")
|
||||||
|
.where("tcf.name = 'assigned_to_id' AND g.group_id=? AND (users.id > 0)", group.id)
|
||||||
|
.order('COUNT(users.id) DESC')
|
||||||
|
.group('users.id')
|
||||||
|
.select('users.*, COUNT(users.id) as "assignments_count"')
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
|
|
||||||
|
render json: { members: serialize_data(members, GroupUserAssignedSerializer) }
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def translate_failure(reason, user)
|
def translate_failure(reason, user)
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class GroupUserAssignedSerializer < BasicUserSerializer
|
||||||
|
|
||||||
|
attributes :assignments_count,
|
||||||
|
:username_lower
|
||||||
|
|
||||||
|
def include_assignments_count
|
||||||
|
object.can_assign?
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1,31 +1,33 @@
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import Controller, { inject as controller } from "@ember/controller";
|
import Controller, { inject as controller } from "@ember/controller";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
router: service(),
|
router: service(),
|
||||||
application: controller(),
|
application: controller(),
|
||||||
loading: false,
|
loading: false,
|
||||||
|
offset: 0,
|
||||||
|
|
||||||
findMembers(refresh) {
|
findMembers(refresh) {
|
||||||
|
if (refresh) {
|
||||||
|
this.set("members", this.model.members);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.loading || !this.model) {
|
if (this.loading || !this.model) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!refresh && this.model.members.length >= this.model.user_count) {
|
if (this.model.members.length >= this.offset + 50) {
|
||||||
this.set("application.showFooter", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.set("loading", true);
|
this.set("loading", true);
|
||||||
this.model
|
this.set("offset", this.offset + 50);
|
||||||
.findMembers({ order: "", asc: true, filter: null }, refresh)
|
ajax(`/assign/members/${this.groupName}?offset=${this.offset}`).then(
|
||||||
.finally(() => {
|
result => {
|
||||||
this.setProperties({
|
this.members.pushObjects(result.members);
|
||||||
"application.showFooter":
|
this.set("loading", false);
|
||||||
this.model.members.length >= this.model.user_count,
|
}
|
||||||
loading: false
|
);
|
||||||
});
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
import Route from "@ember/routing/route";
|
import Route from "@ember/routing/route";
|
||||||
|
import { ajax } from "discourse/lib/ajax";
|
||||||
|
|
||||||
export default Route.extend({
|
export default Route.extend({
|
||||||
model() {
|
model() {
|
||||||
return this.modelFor("group");
|
return ajax(`/assign/members/${this.modelFor("group").get("name")}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
setupController(controller, model) {
|
setupController(controller, model) {
|
||||||
controller.setProperties({
|
controller.setProperties({
|
||||||
model,
|
model,
|
||||||
showing: "members"
|
members: [],
|
||||||
|
groupName: this.modelFor("group").get("name")
|
||||||
});
|
});
|
||||||
|
|
||||||
controller.findMembers(true);
|
controller.findMembers(true);
|
||||||
|
|
|
@ -7,12 +7,12 @@ export default Component.extend({
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"siteSettings.prioritize_username_in_ux",
|
"siteSettings.prioritize_username_in_ux",
|
||||||
"filter.username",
|
"filter.username",
|
||||||
"filter.displayName"
|
"filter.name"
|
||||||
)
|
)
|
||||||
displayName(prioritize_username_in_ux, username, displayName) {
|
displayName(prioritize_username_in_ux, username, name) {
|
||||||
if (prioritize_username_in_ux) {
|
if (prioritize_username_in_ux) {
|
||||||
return username;
|
return username.trim();
|
||||||
}
|
}
|
||||||
return displayName;
|
return (name || username).trim();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{#if show-avatar}}
|
{{#if show-avatar}}
|
||||||
{{#link-to "group.assignments.show" filter.username_lower}}
|
{{#link-to "group.assignments.show" filter.username_lower}}
|
||||||
{{avatar filter avatarTemplatePath="avatar_template" usernamePath="username" imageSize="small"}} {{displayName}}
|
{{avatar filter avatarTemplatePath="avatar_template" usernamePath="username" imageSize="small"}} {{displayName}} ({{filter.assignments_count}})
|
||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#link-to "group.assignments.show" filter}}
|
{{#link-to "group.assignments.show" filter}}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{{#mobile-nav class="activity-nav" desktopClass="action-list activity-list nav-stacked" currentPath=router._router.currentPath}}
|
{{#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")}}
|
{{#load-more selector=".activity-nav li" action=(action "loadMore")}}
|
||||||
{{group-assignments-filter show-avatar=false filter="everyone" routeType=route_type}}
|
{{group-assignments-filter show-avatar=false filter="everyone" routeType=route_type}}
|
||||||
{{#each model.members as |member|}}
|
{{#each members as |member|}}
|
||||||
{{group-assignments-filter show-avatar=true filter=member routeType=route_type}}
|
{{group-assignments-filter show-avatar=true filter=member routeType=route_type}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{conditional-loading-spinner condition=loading}}
|
{{conditional-loading-spinner condition=loading}}
|
||||||
|
|
|
@ -6,4 +6,5 @@ DiscourseAssign::Engine.routes.draw do
|
||||||
put "/unassign" => "assign#unassign"
|
put "/unassign" => "assign#unassign"
|
||||||
get "/suggestions" => "assign#suggestions"
|
get "/suggestions" => "assign#suggestions"
|
||||||
get "/assigned" => "assign#assigned"
|
get "/assigned" => "assign#assigned"
|
||||||
|
get "/members/:group_name" => "assign#group_members"
|
||||||
end
|
end
|
||||||
|
|
|
@ -253,6 +253,8 @@ after_initialize do
|
||||||
page = (params[:page].to_i || 0).to_i
|
page = (params[:page].to_i || 0).to_i
|
||||||
|
|
||||||
group = Group.find_by("name = ?", params[:groupname])
|
group = Group.find_by("name = ?", params[:groupname])
|
||||||
|
guardian.ensure_can_see_group_members!(group)
|
||||||
|
|
||||||
raise Discourse::NotFound unless group
|
raise Discourse::NotFound unless group
|
||||||
raise Discourse::InvalidAccess unless current_user.can_assign?
|
raise Discourse::InvalidAccess unless current_user.can_assign?
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ RSpec.describe DiscourseAssign::AssignController do
|
||||||
let(:post) { Fabricate(:post) }
|
let(:post) { Fabricate(:post) }
|
||||||
let(:user2) { Fabricate(:active_user) }
|
let(:user2) { Fabricate(:active_user) }
|
||||||
let(:nonadmin) { Fabricate(:user, groups: [default_allowed_group]) }
|
let(:nonadmin) { Fabricate(:user, groups: [default_allowed_group]) }
|
||||||
|
let(:normal_user) { Fabricate(:user) }
|
||||||
|
let(:normal_admin) { Fabricate(:admin) }
|
||||||
|
|
||||||
describe 'only allow users from allowed groups' do
|
describe 'only allow users from allowed groups' do
|
||||||
before { sign_in(user2) }
|
before { sign_in(user2) }
|
||||||
|
@ -196,4 +198,56 @@ RSpec.describe DiscourseAssign::AssignController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context '#group_members' do
|
||||||
|
include_context 'A group that is allowed to assign'
|
||||||
|
|
||||||
|
fab!(:post1) { Fabricate(:post) }
|
||||||
|
fab!(:post2) { Fabricate(:post) }
|
||||||
|
fab!(:post3) { Fabricate(:post) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
add_to_assign_allowed_group(user2)
|
||||||
|
add_to_assign_allowed_group(user)
|
||||||
|
|
||||||
|
freeze_time 1.hour.from_now
|
||||||
|
TopicAssigner.new(post1.topic, user).assign(user)
|
||||||
|
|
||||||
|
freeze_time 1.hour.from_now
|
||||||
|
TopicAssigner.new(post2.topic, user).assign(user2)
|
||||||
|
|
||||||
|
freeze_time 1.hour.from_now
|
||||||
|
TopicAssigner.new(post3.topic, user).assign(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'list members order by assignments_count' do
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
|
get "/assign/members/#{get_assigned_allowed_group_name}.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(JSON.parse(response.body)['members'].map { |m| m['id'] }).to match_array([user.id, user2.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't include members with no assignments" do
|
||||||
|
sign_in(user)
|
||||||
|
add_to_assign_allowed_group(nonadmin)
|
||||||
|
|
||||||
|
get "/assign/members/#{get_assigned_allowed_group_name}.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(JSON.parse(response.body)['members'].map { |m| m['id'] }).to match_array([user.id, user2.id])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "404 error to non-group-members" do
|
||||||
|
sign_in(normal_user)
|
||||||
|
|
||||||
|
get "/assign/members/#{get_assigned_allowed_group_name}.json"
|
||||||
|
expect(response.status).to eq(403)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows non-member-admin" do
|
||||||
|
sign_in(normal_admin)
|
||||||
|
|
||||||
|
get "/assign/members/#{get_assigned_allowed_group_name}.json"
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,6 @@ componentTest("display username", {
|
||||||
this.set("filter", {
|
this.set("filter", {
|
||||||
id: 2,
|
id: 2,
|
||||||
username: "Ahmed",
|
username: "Ahmed",
|
||||||
displayName: "Ahmed Gagan",
|
|
||||||
name: "Ahmed Gagan",
|
name: "Ahmed Gagan",
|
||||||
avatar_template: "/letter_avatar_proxy/v4/letter/a/8c91f0/{size}.png",
|
avatar_template: "/letter_avatar_proxy/v4/letter/a/8c91f0/{size}.png",
|
||||||
title: "trust_level_0",
|
title: "trust_level_0",
|
||||||
|
@ -32,7 +31,6 @@ componentTest("display name", {
|
||||||
this.set("filter", {
|
this.set("filter", {
|
||||||
id: 2,
|
id: 2,
|
||||||
username: "Ahmed",
|
username: "Ahmed",
|
||||||
displayName: "Ahmed Gagan",
|
|
||||||
name: "Ahmed Gagan",
|
name: "Ahmed Gagan",
|
||||||
avatar_template: "/letter_avatar_proxy/v4/letter/a/8c91f0/{size}.png",
|
avatar_template: "/letter_avatar_proxy/v4/letter/a/8c91f0/{size}.png",
|
||||||
title: "trust_level_0",
|
title: "trust_level_0",
|
||||||
|
|
Loading…
Reference in New Issue