Merge pull request #5033 from tgxworld/reason_when_requesting_to_join_a_group

FEATURE: Force user to enter reason when requesting for group members…
This commit is contained in:
Guo Xiang Tan 2017-08-09 15:54:21 +09:00 committed by GitHub
commit 898c6ba037
20 changed files with 159 additions and 32 deletions

View File

@ -91,6 +91,17 @@
</label> </label>
</div> </div>
{{#if model.allow_membership_requests}}
<div>
<label for="membership-request-template">
{{i18n 'groups.membership_request_template'}}
</label>
{{expanding-text-area name="membership-request-template"
value=model.membership_request_template}}
</div>
{{/if}}
<div> <div>
<label for="primary_group"> <label for="primary_group">
{{input type="checkbox" checked=model.primary_group}} {{input type="checkbox" checked=model.primary_group}}

View File

@ -1,10 +1,8 @@
import { default as computed } from 'ember-addons/ember-computed-decorators'; import { default as computed } from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error'; import { popupAjaxError } from 'discourse/lib/ajax-error';
import DiscourseURL from 'discourse/lib/url'; import showModal from 'discourse/lib/show-modal';
export default Ember.Component.extend({ export default Ember.Component.extend({
loading: false,
@computed("model.public_admission", "userIsGroupUser") @computed("model.public_admission", "userIsGroupUser")
canJoinGroup(publicAdmission, userIsGroupUser) { canJoinGroup(publicAdmission, userIsGroupUser) {
return publicAdmission && !userIsGroupUser; return publicAdmission && !userIsGroupUser;
@ -56,15 +54,9 @@ export default Ember.Component.extend({
}); });
}, },
requestMembership() { showRequestMembershipForm() {
if (this.currentUser) { if (this.currentUser) {
this.set('loading', true); showModal("request-group-membership-form", { model: this.get('model') });
this.get('model').requestMembership().then(result => {
DiscourseURL.routeTo(result.relative_url);
}).catch(popupAjaxError).finally(() => {
this.set('loading', false);
});
} else { } else {
this._showLoginModal(); this._showLoginModal();
} }

View File

@ -0,0 +1,34 @@
import computed from 'ember-addons/ember-computed-decorators';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import DiscourseURL from 'discourse/lib/url';
export default Ember.Controller.extend({
loading: false,
reason: Ember.computed.alias('model.membership_request_template'),
@computed('model.name')
title(groupName) {
return I18n.t('groups.membership_request.title', { group_name: groupName });
},
@computed('loading', 'reason')
disableSubmit(loading, reason) {
return (loading || Ember.isEmpty(reason));
},
actions: {
requestMember() {
if (this.currentUser) {
this.set('loading', true);
this.get('model').requestMembership(this.get('reason')).then(result => {
DiscourseURL.routeTo(result.relative_url);
}).catch(popupAjaxError).finally(() => {
this.set('loading', false);
});
} else {
this._showLoginModal();
}
}
}
});

View File

@ -147,7 +147,8 @@ const Group = RestModel.extend({
public_exit: this.get('public_exit'), public_exit: this.get('public_exit'),
allow_membership_requests: this.get('allow_membership_requests'), allow_membership_requests: this.get('allow_membership_requests'),
full_name: this.get('full_name'), full_name: this.get('full_name'),
default_notification_level: this.get('default_notification_level') default_notification_level: this.get('default_notification_level'),
membership_request_template: this.get('membership_request_template')
}; };
if (!this.get('id')) { if (!this.get('id')) {
@ -220,9 +221,10 @@ const Group = RestModel.extend({
}); });
}, },
requestMembership() { requestMembership(reason) {
return ajax(`/groups/${this.get('name')}/request_membership`, { return ajax(`/groups/${this.get('name')}/request_membership`, {
type: "POST" type: "POST",
data: { reason: reason }
}); });
}, },
}); });

View File

@ -20,15 +20,11 @@
disabled=true}} disabled=true}}
{{/if}} {{/if}}
{{else}} {{else}}
{{d-button action="requestMembership" {{d-button action="showRequestMembershipForm"
class="group-index-request" class="group-index-request"
disabled=loading disabled=loading
icon="user-plus" icon="user-plus"
label="groups.request"}} label="groups.request"}}
{{#if loading}}
{{loading-spinner size="small"}}
{{/if}}
{{/if}} {{/if}}
{{else}} {{else}}
{{yield}} {{yield}}

View File

@ -51,6 +51,18 @@
</label> </label>
</div> </div>
{{#if model.allow_membership_requests}}
<div>
<label for="membership-request-template">
{{i18n 'groups.membership_request_template'}}
</label>
{{expanding-text-area name="membership-request-template"
value=model.membership_request_template
class="group-edit-membership-request-template"}}
</div>
{{/if}}
{{plugin-outlet name="group-edit" args=(hash group=model)}} {{plugin-outlet name="group-edit" args=(hash group=model)}}
{{d-button action="save" class="btn-primary" disabled=saving label="save"}} {{d-button action="save" class="btn-primary" disabled=saving label="save"}}

View File

@ -0,0 +1,21 @@
<form class='request-group-membership-form'>
{{#d-modal-body rawTitle=title}}
<div class="control-group">
<label>
{{i18n "groups.membership_request.reason"}}
</label>
{{expanding-text-area value=reason}}
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button class="btn-primary"
disabled=disableSubmit
label="groups.membership_request.submit"
action="requestMember"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
{{conditional-loading-spinner size="small" condition=loading}}
</div>
</form>

View File

@ -0,0 +1,5 @@
.request-group-membership-form {
label {
font-weight: bold;
}
}

View File

@ -97,6 +97,7 @@ class Admin::GroupsController < Admin::AdminController
if group_params[:allow_membership_requests] if group_params[:allow_membership_requests]
group.allow_membership_requests = group_params[:allow_membership_requests] group.allow_membership_requests = group_params[:allow_membership_requests]
group.membership_request_template = group_params[:membership_request_template]
end end
if group_params[:owner_usernames].present? if group_params[:owner_usernames].present?
@ -208,7 +209,8 @@ class Admin::GroupsController < Admin::AdminController
:full_name, :full_name,
:default_notification_level, :default_notification_level,
:usernames, :usernames,
:owner_usernames :owner_usernames,
:membership_request_template
) )
end end
end end

View File

@ -243,6 +243,8 @@ class GroupsController < ApplicationController
end end
def request_membership def request_membership
params.require(:reason)
unless current_user.staff? unless current_user.staff?
RateLimiter.new(current_user, "request_group_membership", 1, 1.day).performed! RateLimiter.new(current_user, "request_group_membership", 1, 1.day).performed!
end end
@ -259,7 +261,7 @@ class GroupsController < ApplicationController
post = PostCreator.new(current_user, post = PostCreator.new(current_user,
title: I18n.t('groups.request_membership_pm.title', group_name: group_name), title: I18n.t('groups.request_membership_pm.title', group_name: group_name),
raw: I18n.t('groups.request_membership_pm.body', group_name: group_name), raw: params[:reason],
archetype: Archetype.private_message, archetype: Archetype.private_message,
target_usernames: usernames.join(','), target_usernames: usernames.join(','),
skip_validations: true skip_validations: true

View File

@ -22,7 +22,8 @@ class BasicGroupSerializer < ApplicationSerializer
:public_exit, :public_exit,
:allow_membership_requests, :allow_membership_requests,
:full_name, :full_name,
:default_notification_level :default_notification_level,
:membership_request_template
def include_display_name? def include_display_name?
object.automatic object.automatic

View File

@ -427,6 +427,12 @@ en:
closed_group: Closed Group closed_group: Closed Group
is_group_user: "You are a member of this group" is_group_user: "You are a member of this group"
allow_membership_requests: "Allow users to send membership requests to group owners" allow_membership_requests: "Allow users to send membership requests to group owners"
membership_request_template: "Custom template to display to users when sending a membership request"
membership_request:
submit: "Submit Request"
title: "Request to join @%{group_name}"
reason: "Let the group owners know why you belong in this group"
membership: "Membership" membership: "Membership"
name: "Name" name: "Name"
user_count: "Number of Members" user_count: "Number of Members"

View File

@ -280,7 +280,6 @@ en:
trust_level_4: "trust_level_4" trust_level_4: "trust_level_4"
request_membership_pm: request_membership_pm:
title: "Membership Request for @%{group_name}" title: "Membership Request for @%{group_name}"
body: "I would like to apply for membership in @%{group_name}."
education: education:
until_posts: until_posts:

View File

@ -0,0 +1,5 @@
class AddMembershipRequestTemplateToGroups < ActiveRecord::Migration
def change
add_column :groups, :membership_request_template, :text
end
end

View File

@ -15,7 +15,8 @@ RSpec.describe "Managing groups as an admin" do
name: 'testing', name: 'testing',
usernames: [admin.username, user.username].join(","), usernames: [admin.username, user.username].join(","),
owner_usernames: [user.username].join(","), owner_usernames: [user.username].join(","),
allow_membership_requests: true allow_membership_requests: true,
membership_request_template: 'Testing',
} }
expect(response).to be_success expect(response).to be_success
@ -25,6 +26,7 @@ RSpec.describe "Managing groups as an admin" do
expect(group.name).to eq('testing') expect(group.name).to eq('testing')
expect(group.users).to contain_exactly(admin, user) expect(group.users).to contain_exactly(admin, user)
expect(group.allow_membership_requests).to eq(true) expect(group.allow_membership_requests).to eq(true)
expect(group.membership_request_template).to eq('Testing')
end end
end end

View File

@ -564,6 +564,14 @@ describe "Groups" do
end.to raise_error(Discourse::NotLoggedIn) end.to raise_error(Discourse::NotLoggedIn)
end end
it 'requires a reason' do
sign_in(user)
expect do
xhr :post, "/groups/#{group.name}/request_membership"
end.to raise_error(ActionController::ParameterMissing)
end
it 'should create the right PM' do it 'should create the right PM' do
owner1 = Fabricate(:user, last_seen_at: Time.zone.now) owner1 = Fabricate(:user, last_seen_at: Time.zone.now)
owner2 = Fabricate(:user, last_seen_at: Time.zone.now - 1 .day) owner2 = Fabricate(:user, last_seen_at: Time.zone.now - 1 .day)
@ -571,7 +579,8 @@ describe "Groups" do
sign_in(user) sign_in(user)
xhr :post, "/groups/#{group.name}/request_membership" xhr :post, "/groups/#{group.name}/request_membership",
reason: 'Please add me in'
expect(response).to be_success expect(response).to be_success
@ -586,10 +595,7 @@ describe "Groups" do
group_name: group.name group_name: group.name
)) ))
expect(post.raw).to eq(I18n.t( expect(post.raw).to eq('Please add me in')
'groups.request_membership_pm.body', group_name: group.name
))
expect(topic.archetype).to eq(Archetype.private_message) expect(topic.archetype).to eq(Archetype.private_message)
expect(topic.allowed_users).to contain_exactly(user, owner1, owner2) expect(topic.allowed_users).to contain_exactly(user, owner1, owner2)
expect(topic.allowed_groups).to eq([]) expect(topic.allowed_groups).to eq([])

View File

@ -27,9 +27,7 @@ QUnit.test("Editing group", assert => {
assert.ok(find('.group-members-input .item').length === 7, 'it should display group members'); assert.ok(find('.group-members-input .item').length === 7, 'it should display group members');
assert.ok(find('.group-members-input-selector').length === 1, 'it should display input to add group members'); assert.ok(find('.group-members-input-selector').length === 1, 'it should display input to add group members');
assert.ok(find('.group-members-input-selector .add[disabled]').length === 1, 'add members button should be disabled'); assert.ok(find('.group-members-input-selector .add[disabled]').length === 1, 'add members button should be disabled');
});
andThen(() => {
assert.ok( assert.ok(
find('.group-edit-allow-membership-requests[disabled]').length === 1, find('.group-edit-allow-membership-requests[disabled]').length === 1,
'it should disable group allow_membership_request input' 'it should disable group allow_membership_request input'
@ -44,6 +42,11 @@ QUnit.test("Editing group", assert => {
find('.group-edit-public-admission[disabled]').length === 1, find('.group-edit-public-admission[disabled]').length === 1,
'it should disable group public admission input' 'it should disable group public admission input'
); );
assert.equal(
find('.group-edit-membership-request-template').length, 1,
'it should display the membership request template field'
);
}); });
}); });

View File

@ -85,6 +85,34 @@ QUnit.test("User Viewing Group", assert => {
logIn(); logIn();
Discourse.reset(); Discourse.reset();
visit("/groups");
click('.group-index-request');
server.post('/groups/Macdonald/request_membership', () => { // eslint-disable-line no-undef
return [
200,
{ "Content-Type": "application/json" },
{ relative_url: '/t/internationalization-localization/280' }
];
});
andThen(() => {
assert.equal(find('.modal-header').text().trim(), I18n.t(
'groups.membership_request.title', { group_name: 'Macdonald' }
));
assert.equal(find('.request-group-membership-form textarea').val(), 'Please add me');
});
click('.modal-footer .btn-primary');
andThen(() => {
assert.equal(
find('.fancy-title').text().trim(),
"Internationalization / localization"
);
});
visit("/groups/discourse"); visit("/groups/discourse");
click('.group-message-button'); click('.group-message-button');

View File

@ -1,3 +1,3 @@
export default { export default {
"/groups.json": {"groups":[{"id":41,"automatic":false,"name":"discourse","user_count":0,"alias_level":0,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"has_messages":false,"flair_url":null,"flair_bg_color":null,"flair_color":null,"bio_raw":"","bio_cooked":null,"public_admission":true,"allow_membership_requests":false,"full_name":"Awesome Team"},{"id":42,"automatic":false,"name":"Macdonald","user_count":0,"alias_level":99,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"has_messages":false,"flair_url":null,"flair_bg_color":null,"flair_color":null,"bio_raw":null,"bio_cooked":null,"public_admission":false,"allow_membership_requests":true,"full_name":null}],"extras":{"group_user_ids":[]},"total_rows_groups":2,"load_more_groups":"/groups?page=1"} "/groups.json": {"groups":[{"id":41,"automatic":false,"name":"discourse","user_count":0,"alias_level":0,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"has_messages":false,"flair_url":null,"flair_bg_color":null,"flair_color":null,"bio_raw":"","bio_cooked":null,"public_admission":true,"allow_membership_requests":false,"full_name":"Awesome Team"},{"id":42,"automatic":false,"name":"Macdonald","user_count":0,"alias_level":99,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"has_messages":false,"flair_url":null,"flair_bg_color":null,"flair_color":null,"bio_raw":null,"bio_cooked":null,"public_admission":false,"allow_membership_requests":true,"membership_request_template":"Please add me","full_name":null}],"extras":{"group_user_ids":[]},"total_rows_groups":2,"load_more_groups":"/groups?page=1"}
} }