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:
parent
f26ff290c3
commit
83c549bd31
|
@ -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) {
|
||||
|
|
|
@ -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}}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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"))
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -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"));
|
||||
|
|
|
@ -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>
|
|
@ -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")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
||||
});
|
Loading…
Reference in New Issue