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>
</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>
<label for="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 { popupAjaxError } from 'discourse/lib/ajax-error';
import DiscourseURL from 'discourse/lib/url';
import showModal from 'discourse/lib/show-modal';
export default Ember.Component.extend({
loading: false,
@computed("model.public_admission", "userIsGroupUser")
canJoinGroup(publicAdmission, userIsGroupUser) {
return publicAdmission && !userIsGroupUser;
@ -56,15 +54,9 @@ export default Ember.Component.extend({
});
},
requestMembership() {
showRequestMembershipForm() {
if (this.currentUser) {
this.set('loading', true);
this.get('model').requestMembership().then(result => {
DiscourseURL.routeTo(result.relative_url);
}).catch(popupAjaxError).finally(() => {
this.set('loading', false);
});
showModal("request-group-membership-form", { model: this.get('model') });
} else {
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'),
allow_membership_requests: this.get('allow_membership_requests'),
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')) {
@ -220,9 +221,10 @@ const Group = RestModel.extend({
});
},
requestMembership() {
requestMembership(reason) {
return ajax(`/groups/${this.get('name')}/request_membership`, {
type: "POST"
type: "POST",
data: { reason: reason }
});
},
});

View File

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

View File

@ -51,6 +51,18 @@
</label>
</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)}}
{{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]
group.allow_membership_requests = group_params[:allow_membership_requests]
group.membership_request_template = group_params[:membership_request_template]
end
if group_params[:owner_usernames].present?
@ -208,7 +209,8 @@ class Admin::GroupsController < Admin::AdminController
:full_name,
:default_notification_level,
:usernames,
:owner_usernames
:owner_usernames,
:membership_request_template
)
end
end

View File

@ -243,6 +243,8 @@ class GroupsController < ApplicationController
end
def request_membership
params.require(:reason)
unless current_user.staff?
RateLimiter.new(current_user, "request_group_membership", 1, 1.day).performed!
end
@ -259,7 +261,7 @@ class GroupsController < ApplicationController
post = PostCreator.new(current_user,
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,
target_usernames: usernames.join(','),
skip_validations: true

View File

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

View File

@ -427,6 +427,12 @@ en:
closed_group: Closed Group
is_group_user: "You are a member of this group"
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"
name: "Name"
user_count: "Number of Members"

View File

@ -280,7 +280,6 @@ en:
trust_level_4: "trust_level_4"
request_membership_pm:
title: "Membership Request for @%{group_name}"
body: "I would like to apply for membership in @%{group_name}."
education:
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',
usernames: [admin.username, 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
@ -25,6 +26,7 @@ RSpec.describe "Managing groups as an admin" do
expect(group.name).to eq('testing')
expect(group.users).to contain_exactly(admin, user)
expect(group.allow_membership_requests).to eq(true)
expect(group.membership_request_template).to eq('Testing')
end
end

View File

@ -564,6 +564,14 @@ describe "Groups" do
end.to raise_error(Discourse::NotLoggedIn)
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
owner1 = Fabricate(:user, last_seen_at: Time.zone.now)
owner2 = Fabricate(:user, last_seen_at: Time.zone.now - 1 .day)
@ -571,7 +579,8 @@ describe "Groups" do
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
@ -586,10 +595,7 @@ describe "Groups" do
group_name: group.name
))
expect(post.raw).to eq(I18n.t(
'groups.request_membership_pm.body', group_name: group.name
))
expect(post.raw).to eq('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([])

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-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');
});
andThen(() => {
assert.ok(
find('.group-edit-allow-membership-requests[disabled]').length === 1,
'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,
'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();
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");
click('.group-message-button');

View File

@ -1,3 +1,3 @@
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"}
}