FEATURE: Add page for all group membership requests. (#6909)
This commit is contained in:
parent
ef2362a30f
commit
a9798f0c47
|
@ -0,0 +1,122 @@
|
|||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import Group from "discourse/models/group";
|
||||
import {
|
||||
default as computed,
|
||||
observes
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
import debounce from "discourse/lib/debounce";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
queryParams: ["order", "desc", "filter"],
|
||||
order: "",
|
||||
desc: null,
|
||||
loading: false,
|
||||
limit: null,
|
||||
offset: null,
|
||||
filter: null,
|
||||
filterInput: null,
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
@observes("filterInput")
|
||||
_setFilter: debounce(function() {
|
||||
this.set("filter", this.get("filterInput"));
|
||||
}, 500),
|
||||
|
||||
@observes("order", "desc", "filter")
|
||||
refreshRequesters(force) {
|
||||
if (this.get("loading") || !this.get("model")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!force &&
|
||||
this.get("count") &&
|
||||
this.get("model.requesters.length") >= this.get("count")
|
||||
) {
|
||||
this.set("application.showFooter", true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("loading", true);
|
||||
this.set("application.showFooter", false);
|
||||
|
||||
Group.loadMembers(
|
||||
this.get("model.name"),
|
||||
force ? 0 : this.get("model.requesters.length"),
|
||||
this.get("limit"),
|
||||
{
|
||||
order: this.get("order"),
|
||||
desc: this.get("desc"),
|
||||
filter: this.get("filter"),
|
||||
requesters: true
|
||||
}
|
||||
).then(result => {
|
||||
const requesters = (!force && this.get("model.requesters")) || [];
|
||||
requesters.addObjects(result.members.map(m => Discourse.User.create(m)));
|
||||
this.set("model.requesters", requesters);
|
||||
|
||||
this.setProperties({
|
||||
loading: false,
|
||||
count: result.meta.total,
|
||||
limit: result.meta.limit,
|
||||
offset: Math.min(
|
||||
result.meta.offset + result.meta.limit,
|
||||
result.meta.total
|
||||
)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@computed("model.requesters")
|
||||
hasRequesters(requesters) {
|
||||
return requesters && requesters.length > 0;
|
||||
},
|
||||
|
||||
@computed
|
||||
filterPlaceholder() {
|
||||
if (this.currentUser && this.currentUser.admin) {
|
||||
return "groups.members.filter_placeholder_admin";
|
||||
} else {
|
||||
return "groups.members.filter_placeholder";
|
||||
}
|
||||
},
|
||||
|
||||
handleRequest(data) {
|
||||
ajax(`/groups/${this.get("model.id")}/handle_membership_request.json`, {
|
||||
data,
|
||||
type: "PUT"
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
this.refreshRequesters();
|
||||
},
|
||||
|
||||
acceptRequest(user) {
|
||||
this.handleRequest({ user_id: user.get("id"), accept: true });
|
||||
user.setProperties({
|
||||
request_accepted: true,
|
||||
request_denied: false
|
||||
});
|
||||
},
|
||||
|
||||
undoAcceptRequest(user) {
|
||||
ajax("/groups/" + this.get("model.id") + "/members.json", {
|
||||
type: "DELETE",
|
||||
data: { user_id: user.get("id") }
|
||||
}).then(() => {
|
||||
user.set("request_undone", true);
|
||||
});
|
||||
},
|
||||
|
||||
denyRequest(user) {
|
||||
this.handleRequest({ user_id: user.get("id") });
|
||||
user.setProperties({
|
||||
request_accepted: false,
|
||||
request_denied: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -15,8 +15,13 @@ export default Ember.Controller.extend({
|
|||
showing: "members",
|
||||
destroying: null,
|
||||
|
||||
@computed("showMessages", "model.user_count", "canManageGroup")
|
||||
tabs(showMessages, userCount, canManageGroup) {
|
||||
@computed(
|
||||
"showMessages",
|
||||
"model.user_count",
|
||||
"canManageGroup",
|
||||
"model.allow_membership_requests"
|
||||
)
|
||||
tabs(showMessages, userCount, canManageGroup, allowMembershipRequests) {
|
||||
const membersTab = Tab.create({
|
||||
name: "members",
|
||||
route: "group.index",
|
||||
|
@ -28,6 +33,16 @@ export default Ember.Controller.extend({
|
|||
|
||||
const defaultTabs = [membersTab, Tab.create({ name: "activity" })];
|
||||
|
||||
if (canManageGroup && allowMembershipRequests) {
|
||||
defaultTabs.push(
|
||||
Tab.create({
|
||||
name: "requests",
|
||||
i18nKey: "requests.title",
|
||||
icon: "user-plus"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (showMessages) {
|
||||
defaultTabs.push(
|
||||
Tab.create({
|
||||
|
|
|
@ -66,6 +66,7 @@ export default function() {
|
|||
|
||||
this.route("group", { path: "/g/:name", resetNamespace: true }, function() {
|
||||
this.route("members");
|
||||
this.route("requests");
|
||||
|
||||
this.route("activity", function() {
|
||||
this.route("posts");
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
export default Discourse.Route.extend({
|
||||
titleToken() {
|
||||
return I18n.t("groups.requests.title");
|
||||
},
|
||||
|
||||
model(params) {
|
||||
this._params = params;
|
||||
return this.modelFor("group");
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this.controllerFor("group").set("showing", "requests");
|
||||
|
||||
controller.setProperties({
|
||||
model,
|
||||
filterInput: this._params.filter
|
||||
});
|
||||
|
||||
controller.refreshRequesters(true);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,51 @@
|
|||
<div class="group-members-actions">
|
||||
{{text-field value=filterInput
|
||||
placeholderKey=filterPlaceholder
|
||||
class="group-username-filter no-blur"}}
|
||||
</div>
|
||||
|
||||
{{#if hasRequesters}}
|
||||
{{#load-more selector=".group-members tr" action=(action "loadMore")}}
|
||||
<table class='group-members'>
|
||||
<thead>
|
||||
{{group-index-toggle order=order desc=desc field='username_lower' i18nKey='username'}}
|
||||
{{group-index-toggle order=order desc=desc field='requested_at' i18nKey='groups.member_requested'}}
|
||||
<th>{{i18n "groups.requests.reason"}}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{{#each model.requesters as |m|}}
|
||||
<tr>
|
||||
<td class='avatar'>
|
||||
{{user-info user=m skipName=skipName}}
|
||||
</td>
|
||||
<td>
|
||||
<span class="text">{{bound-date m.requested_at}}</span>
|
||||
</td>
|
||||
<td>{{m.reason}}</td>
|
||||
<td>
|
||||
{{#if m.request_undone}}
|
||||
{{i18n "groups.requests.undone"}}
|
||||
{{else if m.request_accepted}}
|
||||
{{i18n "groups.requests.accepted"}}
|
||||
{{d-button action=(action "undoAcceptRequest") actionParam=m label="undo"}}
|
||||
{{else if m.request_denied}}
|
||||
{{i18n "groups.requests.denied"}}
|
||||
{{else}}
|
||||
{{d-button action=(action "acceptRequest") actionParam=m label="groups.requests.accept" class="btn-primary"}}
|
||||
{{d-button action=(action "denyRequest") actionParam=m label="groups.requests.deny" class="btn-danger"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/load-more}}
|
||||
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
{{else}}
|
||||
<div>{{i18n "groups.empty.requests"}}</div>
|
||||
{{/if}}
|
|
@ -214,6 +214,39 @@ class GroupsController < ApplicationController
|
|||
dir = (params[:desc] && !params[:desc].blank?) ? 'DESC' : 'ASC'
|
||||
order = ""
|
||||
|
||||
if params[:requesters]
|
||||
guardian.ensure_can_edit!(group)
|
||||
|
||||
users = group.requesters
|
||||
total = users.count
|
||||
|
||||
if (filter = params[:filter]).present?
|
||||
filter = filter.split(',') if filter.include?(',')
|
||||
|
||||
if current_user&.admin
|
||||
users = users.filter_by_username_or_email(filter)
|
||||
else
|
||||
users = users.filter_by_username(filter)
|
||||
end
|
||||
end
|
||||
|
||||
users = users
|
||||
.select("users.*, group_requests.reason, group_requests.created_at requested_at")
|
||||
.order(params[:order] == 'requested_at' ? "group_requests.created_at #{dir}" : "")
|
||||
.order(username_lower: dir)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
return render json: {
|
||||
members: serialize_data(users, GroupRequesterSerializer),
|
||||
meta: {
|
||||
total: total,
|
||||
limit: limit,
|
||||
offset: offset
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
if params[:order] && %w{last_posted_at last_seen_at}.include?(params[:order])
|
||||
order = "#{params[:order]} #{dir} NULLS LAST"
|
||||
elsif params[:order] == 'added_at'
|
||||
|
@ -294,6 +327,26 @@ class GroupsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def handle_membership_request
|
||||
group = Group.find_by(id: params[:id])
|
||||
raise Discourse::InvalidParameters.new(:id) if group.blank?
|
||||
guardian.ensure_can_edit!(group)
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
user = User.find_by(id: params[:user_id])
|
||||
raise Discourse::InvalidParameters.new(:user_id) if user.blank?
|
||||
|
||||
if params[:accept]
|
||||
group.add(user)
|
||||
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
||||
end
|
||||
|
||||
GroupRequest.where(group_id: group.id, user_id: user.id).delete_all
|
||||
end
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def mentionable
|
||||
group = find_group(:group_id, ensure_can_see: false)
|
||||
|
||||
|
@ -369,14 +422,25 @@ class GroupsController < ApplicationController
|
|||
.pluck("users.username")
|
||||
)
|
||||
|
||||
raw = <<~EOF
|
||||
#{params[:reason]}
|
||||
|
||||
---
|
||||
<a href="#{Discourse.base_uri}/g/#{group.name}/requests">
|
||||
#{I18n.t('groups.request_membership_pm.handle')}
|
||||
</a>
|
||||
EOF
|
||||
|
||||
post = PostCreator.new(current_user,
|
||||
title: I18n.t('groups.request_membership_pm.title', group_name: group_name),
|
||||
raw: params[:reason],
|
||||
raw: raw,
|
||||
archetype: Archetype.private_message,
|
||||
target_usernames: usernames.join(','),
|
||||
skip_validations: true
|
||||
).create!
|
||||
|
||||
GroupRequest.create!(group: group, user: current_user, reason: params[:reason])
|
||||
|
||||
render json: success_json.merge(relative_url: post.topic.relative_url)
|
||||
end
|
||||
|
||||
|
|
|
@ -12,12 +12,14 @@ class Group < ActiveRecord::Base
|
|||
|
||||
has_many :category_groups, dependent: :destroy
|
||||
has_many :group_users, dependent: :destroy
|
||||
has_many :group_requests, dependent: :destroy
|
||||
has_many :group_mentions, dependent: :destroy
|
||||
|
||||
has_many :group_archived_messages, dependent: :destroy
|
||||
|
||||
has_many :categories, through: :category_groups
|
||||
has_many :users, through: :group_users
|
||||
has_many :requesters, through: :group_requests, source: :user
|
||||
has_many :group_histories, dependent: :destroy
|
||||
|
||||
has_and_belongs_to_many :web_hooks
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
class GroupRequest < ActiveRecord::Base
|
||||
belongs_to :group
|
||||
belongs_to :user
|
||||
end
|
|
@ -55,6 +55,7 @@ class User < ActiveRecord::Base
|
|||
|
||||
has_many :group_users, dependent: :destroy
|
||||
has_many :groups, through: :group_users
|
||||
has_many :group_requests, dependent: :destroy
|
||||
has_many :secure_categories, through: :groups, source: :categories
|
||||
|
||||
has_many :user_uploads, dependent: :destroy
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
class GroupRequesterSerializer < BasicUserSerializer
|
||||
attributes :reason, :requested_at
|
||||
end
|
|
@ -430,10 +430,19 @@ en:
|
|||
|
||||
groups:
|
||||
member_added: "Added"
|
||||
member_requested: "Requested at"
|
||||
add_members:
|
||||
title: "Add Members"
|
||||
description: "Manage the membership of this group"
|
||||
usernames: "Usernames"
|
||||
requests:
|
||||
title: "Requests"
|
||||
reason: "Reason"
|
||||
accept: "Accept"
|
||||
accepted: "accepted"
|
||||
deny: "Deny"
|
||||
denied: "denied"
|
||||
undone: "request undone"
|
||||
manage:
|
||||
title: "Manage"
|
||||
name: "Name"
|
||||
|
@ -464,6 +473,7 @@ en:
|
|||
empty:
|
||||
posts: "There are no posts by members of this group."
|
||||
members: "There are no members in this group."
|
||||
requests: "There are no membership requests for this group."
|
||||
mentions: "There are no mentions of this group."
|
||||
messages: "There are no messages for this group."
|
||||
topics: "There are no topics by members of this group."
|
||||
|
|
|
@ -379,6 +379,7 @@ en:
|
|||
trust_level_4: "trust_level_4"
|
||||
request_membership_pm:
|
||||
title: "Membership Request for @%{group_name}"
|
||||
handle: "handle membership request"
|
||||
|
||||
education:
|
||||
until_posts:
|
||||
|
|
|
@ -511,6 +511,7 @@ Discourse::Application.routes.draw do
|
|||
%w{
|
||||
activity
|
||||
activity/:filter
|
||||
requests
|
||||
messages
|
||||
messages/inbox
|
||||
messages/archive
|
||||
|
@ -527,6 +528,7 @@ Discourse::Application.routes.draw do
|
|||
put "members" => "groups#add_members"
|
||||
delete "members" => "groups#remove_member"
|
||||
post "request_membership" => "groups#request_membership"
|
||||
put "handle_membership_request" => "groups#handle_membership_request"
|
||||
post "notifications" => "groups#set_notifications"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
class CreateGroupRequests < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :group_requests do |t|
|
||||
t.integer :group_id
|
||||
t.integer :user_id
|
||||
t.text :reason
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :group_requests, :group_id
|
||||
add_index :group_requests, :user_id
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
Fabricator(:group_request) do
|
||||
user
|
||||
group
|
||||
reason { sequence(:reason) { |n| "group request #{n}" } }
|
||||
end
|
|
@ -724,6 +724,20 @@ describe GroupsController do
|
|||
.to contain_exactly(user1.id, user2.id, user3.id)
|
||||
end
|
||||
|
||||
it "can show group requests" do
|
||||
sign_in(Fabricate(:admin))
|
||||
|
||||
user4 = Fabricate(:user)
|
||||
request4 = Fabricate(:group_request, user: user4, group: group)
|
||||
|
||||
get "/groups/#{group.name}/members.json", params: { requesters: true }
|
||||
|
||||
members = JSON.parse(response.body)["members"]
|
||||
expect(members.length).to eq(1)
|
||||
expect(members.first["username"]).to eq(user4.username)
|
||||
expect(members.first["reason"]).to eq(request4.reason)
|
||||
end
|
||||
|
||||
describe 'filterable' do
|
||||
describe 'as a normal user' do
|
||||
it "should not allow members to be filterable by email" do
|
||||
|
@ -1284,7 +1298,7 @@ describe GroupsController do
|
|||
group_name: group.name
|
||||
))
|
||||
|
||||
expect(post.raw).to eq('Please add me in')
|
||||
expect(post.raw).to start_with('Please add me in')
|
||||
expect(topic.archetype).to eq(Archetype.private_message)
|
||||
expect(topic.allowed_users).to contain_exactly(user, owner1, owner2)
|
||||
expect(topic.allowed_groups).to eq([])
|
||||
|
|
|
@ -0,0 +1,130 @@
|
|||
import { acceptance } from "helpers/qunit-helpers";
|
||||
import { parsePostData } from "helpers/create-pretender";
|
||||
|
||||
let requests = [];
|
||||
|
||||
acceptance("Group Requests", {
|
||||
loggedIn: true,
|
||||
pretend(server, helper) {
|
||||
server.get("/groups/Macdonald.json", () => {
|
||||
return helper.response({
|
||||
group: {
|
||||
id: 42,
|
||||
automatic: false,
|
||||
name: "Macdonald",
|
||||
user_count: 1,
|
||||
mentionable_level: 0,
|
||||
messageable_level: 0,
|
||||
visibility_level: 0,
|
||||
automatic_membership_email_domains: "",
|
||||
automatic_membership_retroactive: false,
|
||||
primary_group: false,
|
||||
title: "Macdonald",
|
||||
grant_trust_level: null,
|
||||
incoming_email: null,
|
||||
has_messages: false,
|
||||
flair_url: null,
|
||||
flair_bg_color: "",
|
||||
flair_color: "",
|
||||
bio_raw: null,
|
||||
bio_cooked: null,
|
||||
public_admission: false,
|
||||
public_exit: false,
|
||||
allow_membership_requests: true,
|
||||
full_name: "Macdonald",
|
||||
default_notification_level: 3,
|
||||
membership_request_template: "",
|
||||
is_group_user: true,
|
||||
is_group_owner: true,
|
||||
is_group_owner_display: true,
|
||||
mentionable: false,
|
||||
messageable: false
|
||||
},
|
||||
extras: {
|
||||
visible_group_names: ["discourse", "Macdonald"]
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.get("/groups/Macdonald/members.json", () => {
|
||||
return helper.response({
|
||||
members: [
|
||||
{
|
||||
id: 19,
|
||||
username: "eviltrout",
|
||||
name: "Robin Ward",
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/eviltrout/{size}/5275_2.png",
|
||||
reason: "Please accept my membership request.",
|
||||
requested_at: "2019-01-31T12:00:00.000Z"
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
username: "eviltrout2",
|
||||
name: "Robin Ward",
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/eviltrout/{size}/5275_2.png",
|
||||
reason: "Please accept another membership request.",
|
||||
requested_at: "2019-01-31T14:00:00.000Z"
|
||||
}
|
||||
],
|
||||
meta: { total: 2, limit: 50, offset: 0 }
|
||||
});
|
||||
});
|
||||
|
||||
server.put("/groups/42/handle_membership_request.json", request => {
|
||||
const body = parsePostData(request.requestBody);
|
||||
requests.push([body["user_id"], body["accept"]]);
|
||||
return helper.success();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("Group Requests", async assert => {
|
||||
await visit("/groups/Macdonald/requests");
|
||||
|
||||
assert.equal(find(".group-members tr").length, 2);
|
||||
assert.equal(
|
||||
find(".group-members tr:first-child td:nth-child(1)")
|
||||
.text()
|
||||
.trim()
|
||||
.replace(/\s+/g, " "),
|
||||
"eviltrout Robin Ward"
|
||||
);
|
||||
assert.equal(
|
||||
find(".group-members tr:first-child td:nth-child(3)")
|
||||
.text()
|
||||
.trim(),
|
||||
"Please accept my membership request."
|
||||
);
|
||||
assert.equal(
|
||||
find(".group-members tr:first-child .btn-primary")
|
||||
.text()
|
||||
.trim(),
|
||||
"Accept"
|
||||
);
|
||||
assert.equal(
|
||||
find(".group-members tr:first-child .btn-danger")
|
||||
.text()
|
||||
.trim(),
|
||||
"Deny"
|
||||
);
|
||||
|
||||
await click(".group-members tr:first-child .btn-primary");
|
||||
assert.ok(
|
||||
find(".group-members tr:first-child td:nth-child(4)")
|
||||
.text()
|
||||
.trim()
|
||||
.indexOf("accepted") === 0
|
||||
);
|
||||
assert.deepEqual(requests, [["19", "true"]]);
|
||||
|
||||
await click(".group-members tr:last-child .btn-danger");
|
||||
assert.equal(
|
||||
find(".group-members tr:last-child td:nth-child(4)")
|
||||
.text()
|
||||
.trim(),
|
||||
"denied"
|
||||
);
|
||||
assert.deepEqual(requests, [["19", "true"], ["20", undefined]]);
|
||||
});
|
Loading…
Reference in New Issue