FEATURE: grant badges in post admin wrench (#5498)

* FEATURE: grant badges in post admin wrench

* only grant manually grantable badges

* extract GrantBadgeController mixin
This commit is contained in:
Kyle Zhao 2018-01-21 22:10:53 -05:00 committed by Sam
parent f26ff290c3
commit 83c549bd31
16 changed files with 229 additions and 55 deletions

View File

@ -1,8 +1,10 @@
import UserBadge from 'discourse/models/user-badge';
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
export default Ember.Controller.extend({
export default Ember.Controller.extend(GrantBadgeController, {
adminUser: Ember.inject.controller(),
user: Ember.computed.alias('adminUser.model'),
userBadges: Ember.computed.alias('model'),
allBadges: Ember.computed.alias('badges'),
sortedBadges: Ember.computed.sort('model', 'badgeSortOrder'),
badgeSortOrder: ['granted_at:desc'],
@ -41,36 +43,6 @@ export default Ember.Controller.extend({
return _(expanded).sortBy(group => group.granted_at).reverse().value();
}.property('model', 'model.[]', 'model.expandedBadges.[]'),
/**
Array of badges that have not been granted to this user.
@property grantableBadges
@type {Boolean}
**/
grantableBadges: function() {
var granted = {};
this.get('model').forEach(function(userBadge) {
granted[userBadge.get('badge_id')] = true;
});
var badges = [];
this.get('badges').forEach(function(badge) {
if (badge.get('enabled') && (badge.get('multiple_grant') || !granted[badge.get('id')])) {
badges.push(badge);
}
});
return _.sortBy(badges, badge => badge.get('name'));
}.property('badges.[]', 'model.[]'),
/**
Whether there are any badges that can be granted.
@property noBadges
@type {Boolean}
**/
noBadges: Em.computed.empty('grantableBadges'),
actions: {
expandGroup: function(userBadge){
@ -79,21 +51,21 @@ export default Ember.Controller.extend({
model.get('expandedBadges').pushObject(userBadge.badge.id);
},
grantBadge(badgeId) {
UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(userBadge => {
this.set('badgeReason', '');
this.get('model').pushObject(userBadge);
Ember.run.next(() => {
// Update the selected badge ID after the combobox has re-rendered.
const newSelectedBadge = this.get('grantableBadges')[0];
if (newSelectedBadge) {
this.set('selectedBadgeId', newSelectedBadge.get('id'));
}
grantBadge() {
this.grantBadge(this.get('selectedBadgeId'), this.get('user.username'), this.get('badgeReason'))
.then(() => {
this.set('badgeReason', '');
Ember.run.next(() => {
// Update the selected badge ID after the combobox has re-rendered.
const newSelectedBadge = this.get('grantableBadges')[0];
if (newSelectedBadge) {
this.set('selectedBadgeId', newSelectedBadge.get('id'));
}
});
}, function() {
// Failure
bootbox.alert(I18n.t('generic_error'));
});
}, function() {
// Failure
bootbox.alert(I18n.t('generic_error'));
});
},
revokeBadge(userBadge) {

View File

@ -10,7 +10,7 @@
<div class='admin-container user-badges'>
<h2>{{i18n 'admin.badges.grant_badge'}}</h2>
<br>
{{#if noBadges}}
{{#if noGrantableBadges}}
<p>{{i18n 'admin.badges.no_badges'}}</p>
{{else}}
<form class="form-horizontal">
@ -22,7 +22,7 @@
<label>{{i18n 'admin.badges.reason'}}</label>
{{input type="text" value=badgeReason}}<br><small>{{i18n 'admin.badges.reason_help'}}</small>
</label>
<button class='btn btn-primary' {{action "grantBadge" selectedBadgeId}}>{{i18n 'admin.badges.grant'}}</button>
<button class='btn btn-primary' {{action "grantBadge"}}>{{i18n 'admin.badges.grant'}}</button>
</form>
{{/if}}

View File

@ -60,9 +60,9 @@
//= require ./discourse/models/user-action
//= require ./discourse/models/draft
//= require ./discourse/models/composer
//= require ./discourse/models/user-badge
//= require_tree ./discourse/mixins
//= require ./discourse/models/invite
//= require ./discourse/models/user-badge
//= require ./discourse/controllers/discovery-sortable
//= require ./discourse/controllers/navigation/default
//= require ./discourse/components/edit-category-panel

View File

@ -0,0 +1,63 @@
import computed from "ember-addons/ember-computed-decorators";
import { extractError } from 'discourse/lib/ajax-error';
import ModalFunctionality from "discourse/mixins/modal-functionality";
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import Badge from 'discourse/models/badge';
import UserBadge from 'discourse/models/user-badge';
export default Ember.Controller.extend(ModalFunctionality, GrantBadgeController, {
topicController: Ember.inject.controller("topic"),
loading: true,
saving: false,
selectedBadgeId: null,
allBadges: [],
userBadges: [],
@computed('topicController.selectedPosts')
post() {
return this.get('topicController.selectedPosts')[0];
},
@computed('post')
badgeReason(post) {
const url = post.get('url');
const protocolAndHost = window.location.protocol + '//' + window.location.host;
return url.indexOf('/') === 0 ? protocolAndHost + url : url;
},
@computed("saving", "selectedBadgeGrantable")
buttonDisabled(saving, selectedBadgeGrantable) {
return saving || !selectedBadgeGrantable;
},
onShow() {
this.set('loading', true);
Ember.RSVP.all([Badge.findAll(), UserBadge.findByUsername(this.get('post.username'))])
.then(([allBadges, userBadges]) => {
this.setProperties({
'allBadges': allBadges,
'userBadges': userBadges,
'loading': false,
});
});
},
actions: {
grantBadge() {
this.set('saving', true);
this.grantBadge(this.get('selectedBadgeId'), this.get('post.username'), this.get('badgeReason'))
.then(newBadge => {
this.set('selectedBadgeId', null);
this.flash(I18n.t(
'badges.successfully_granted', { username: this.get('post.username'), badge: newBadge.get('badge.name') }
), 'success');
}, error => {
this.flash(extractError(error), 'error');
})
.finally(() => this.set('saving', false));
}
}
});

View File

@ -518,6 +518,11 @@ export default Ember.Controller.extend(BufferedContent, {
this.send('changeOwner');
},
grantBadge(post) {
this.set("selectedPostIds", [post.id]);
this.send('showGrantBadgeModal');
},
toggleParticipant(user) {
this.get("model.postStream")
.toggleParticipant(user.get("username"))

View File

@ -0,0 +1,38 @@
import computed from "ember-addons/ember-computed-decorators";
import UserBadge from 'discourse/models/user-badge';
export default Ember.Mixin.create({
@computed('allBadges.[]', 'userBadges.[]')
grantableBadges(allBadges, userBadges) {
const granted = userBadges.reduce((map, badge) => {
map[badge.get('badge_id')] = true;
return map;
}, {});
return allBadges
.filter(badge => {
return badge.get('enabled')
&& badge.get('manually_grantable')
&& (!granted[badge.get('id')] || badge.get('multiple_grant'));
})
.sort((a, b) => a.get('name').localeCompare(b.get('name')));
},
noGrantableBadges: Ember.computed.empty('grantableBadges'),
@computed('selectedBadgeId', 'grantableBadges')
selectedBadgeGrantable(selectedBadgeId, grantableBadges) {
return grantableBadges && grantableBadges.find(badge => badge.get('id') === selectedBadgeId);
},
grantBadge(selectedBadgeId, username, badgeReason) {
return UserBadge.grant(selectedBadgeId, username, badgeReason)
.then(newBadge => {
this.get('userBadges').pushObject(newBadge);
return newBadge;
}, error => {
throw error;
});
}
});

View File

@ -85,6 +85,10 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor('modal').set('modalClass', 'history-modal');
},
showGrantBadgeModal() {
showModal('grant-badge', { model: this.modelFor('topic'), title: 'admin.badges.grant_badge' });
},
showRawEmail(model) {
showModal('raw-email', { model });
this.controllerFor('raw_email').loadRawEmail(model.get("id"));

View File

@ -0,0 +1,15 @@
{{#d-modal-body class='grant-badge'}}
{{#conditional-loading-spinner condition=loading}}
{{#if noGrantableBadges}}
<p>{{i18n 'admin.badges.no_badges'}}</p>
{{else}}
<p>{{combo-box filterable=true value=selectedBadgeId content=grantableBadges none="badges.none"}}</p>
{{/if}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}
<div class="modal-footer">
<button class='btn btn-primary' disabled={{buttonDisabled}} {{action "grantBadge"}}>
{{i18n 'admin.badges.grant'}}
</button>
</div>

View File

@ -170,6 +170,7 @@
togglePostType=(action "togglePostType")
rebakePost=(action "rebakePost")
changePostOwner=(action "changePostOwner")
grantBadge=(action "grantBadge")
unhidePost=(action "unhidePost")
replyToPost=(action "replyToPost")
toggleWiki=(action "toggleWiki")

View File

@ -64,6 +64,13 @@ export function buildManageButtons(attrs, currentUser) {
action: 'changePostOwner',
className: 'change-owner'
});
contents.push({
icon: 'certificate',
label: 'post.controls.grant_badge',
action: 'grantBadge',
className: 'grant-badge'
});
}
if (attrs.canManage || attrs.canWiki) {

View File

@ -219,6 +219,10 @@ class Badge < ActiveRecord::Base
Slug.for(self.display_name, '-')
end
def manually_grantable?
query.blank? && !system?
end
protected
def ensure_not_system

View File

@ -1,7 +1,7 @@
class BadgeSerializer < ApplicationSerializer
attributes :id, :name, :description, :grant_count, :allow_title,
:multiple_grant, :icon, :image, :listable, :enabled, :badge_grouping_id,
:system, :long_description, :slug, :has_badge
:system, :long_description, :slug, :has_badge, :manually_grantable?
has_one :badge_type

View File

@ -1979,6 +1979,7 @@ en:
rebake: "Rebuild HTML"
unhide: "Unhide"
change_owner: "Change Ownership"
grant_badge: "Grant Badge"
actions:
flag: 'Flag'
@ -2487,6 +2488,7 @@ en:
other: "%{count} granted"
select_badge_for_title: Select a badge to use as your title
none: "(none)"
successfully_granted: "Successfully granted %{badge} to %{username}"
badge_grouping:
getting_started:
name: Getting Started

View File

@ -61,4 +61,23 @@ describe Badge do
expect(b.grant_count).to eq(1)
end
describe '#manually_grantable?' do
let(:badge) { Fabricate(:badge, name: 'Test Badge') }
subject { badge.manually_grantable? }
context 'when system badge' do
before { badge.system = true }
it { is_expected.to be false }
end
context 'when has query' do
before { badge.query = 'SELECT id FROM users' }
it { is_expected.to be false }
end
context 'when neither system nor has query' do
before { badge.update_columns(system: false, query: nil) }
it { is_expected.to be true }
end
end
end

View File

@ -9,11 +9,17 @@ moduleFor('controller:admin-user-badges', {
});
QUnit.test("grantableBadges", function(assert) {
const badgeFirst = Badge.create({id: 3, name: "A Badge", enabled: true});
const badgeMiddle = Badge.create({id: 1, name: "My Badge", enabled: true});
const badgeLast = Badge.create({id: 2, name: "Zoo Badge", enabled: true});
const badgeDisabled = Badge.create({id: 4, name: "Disabled Badge", enabled: false});
const controller = this.subject({ model: [], badges: [badgeLast, badgeFirst, badgeMiddle, badgeDisabled] });
const badgeFirst = Badge.create({ id: 3, name: "A Badge", enabled: true, manually_grantable: true });
const badgeMiddle = Badge.create({ id: 1, name: "My Badge", enabled: true, manually_grantable: true });
const badgeLast = Badge.create({ id: 2, name: "Zoo Badge", enabled: true, manually_grantable: true });
const badgeDisabled = Badge.create({ id: 4, name: "Disabled Badge", enabled: false, manually_grantable: true });
const badgeAutomatic = Badge.create({ id: 5, name: "Automatic Badge", enabled: true, manually_grantable: false });
const controller = this.subject({
model: [],
badges: [badgeLast, badgeFirst, badgeMiddle, badgeDisabled, badgeAutomatic]
});
const sortedNames = [badgeFirst.name, badgeMiddle.name, badgeLast.name];
const badgeNames = controller.get('grantableBadges').map(function(badge) {
return badge.name;

View File

@ -0,0 +1,38 @@
import GrantBadgeControllerMixin from 'discourse/mixins/grant-badge-controller';
import Badge from 'discourse/models/badge';
QUnit.module('mixin:grant-badge-controller', {
before: function() {
this.GrantBadgeController = Ember.Controller.extend(GrantBadgeControllerMixin);
this.badgeFirst = Badge.create({ id: 3, name: 'A Badge', enabled: true, manually_grantable: true });
this.badgeMiddle = Badge.create({ id: 1, name: 'My Badge', enabled: true, manually_grantable: true });
this.badgeLast = Badge.create({ id: 2, name: 'Zoo Badge', enabled: true, manually_grantable: true });
this.badgeDisabled = Badge.create({ id: 4, name: 'Disabled Badge', enabled: false, manually_grantable: true });
this.badgeAutomatic = Badge.create({ id: 5, name: 'Automatic Badge', enabled: true, manually_grantable: false });
},
beforeEach: function() {
this.subject = this.GrantBadgeController.create({
userBadges: [],
allBadges: [this.badgeLast, this.badgeFirst, this.badgeMiddle, this.badgeDisabled, this.badgeAutomatic],
});
}
});
QUnit.test('grantableBadges', function(assert) {
const sortedNames = [this.badgeFirst.name, this.badgeMiddle.name, this.badgeLast.name];
const badgeNames = this.subject.get('grantableBadges').map(badge => badge.name);
assert.not(badgeNames.includes(this.badgeDisabled), 'excludes disabled badges');
assert.not(badgeNames.includes(this.badgeAutomatic), 'excludes automatic badges');
assert.deepEqual(badgeNames, sortedNames, 'sorts badges by name');
});
QUnit.test('selectedBadgeGrantable', function(assert) {
this.subject.set('selectedBadgeId', this.badgeDisabled.id);
assert.not(this.subject.get('selectedBadgeGrantable'));
this.subject.set('selectedBadgeId', this.badgeFirst.id);
assert.ok(this.subject.get('selectedBadgeGrantable'));
});