mirror of
https://github.com/discourse/discourse.git
synced 2025-05-24 14:12:10 +08:00
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:
@ -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(),
|
adminUser: Ember.inject.controller(),
|
||||||
user: Ember.computed.alias('adminUser.model'),
|
user: Ember.computed.alias('adminUser.model'),
|
||||||
|
userBadges: Ember.computed.alias('model'),
|
||||||
|
allBadges: Ember.computed.alias('badges'),
|
||||||
|
|
||||||
sortedBadges: Ember.computed.sort('model', 'badgeSortOrder'),
|
sortedBadges: Ember.computed.sort('model', 'badgeSortOrder'),
|
||||||
badgeSortOrder: ['granted_at:desc'],
|
badgeSortOrder: ['granted_at:desc'],
|
||||||
@ -41,36 +43,6 @@ export default Ember.Controller.extend({
|
|||||||
return _(expanded).sortBy(group => group.granted_at).reverse().value();
|
return _(expanded).sortBy(group => group.granted_at).reverse().value();
|
||||||
}.property('model', 'model.[]', 'model.expandedBadges.[]'),
|
}.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: {
|
actions: {
|
||||||
|
|
||||||
expandGroup: function(userBadge){
|
expandGroup: function(userBadge){
|
||||||
@ -79,10 +51,10 @@ export default Ember.Controller.extend({
|
|||||||
model.get('expandedBadges').pushObject(userBadge.badge.id);
|
model.get('expandedBadges').pushObject(userBadge.badge.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
grantBadge(badgeId) {
|
grantBadge() {
|
||||||
UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(userBadge => {
|
this.grantBadge(this.get('selectedBadgeId'), this.get('user.username'), this.get('badgeReason'))
|
||||||
|
.then(() => {
|
||||||
this.set('badgeReason', '');
|
this.set('badgeReason', '');
|
||||||
this.get('model').pushObject(userBadge);
|
|
||||||
Ember.run.next(() => {
|
Ember.run.next(() => {
|
||||||
// Update the selected badge ID after the combobox has re-rendered.
|
// Update the selected badge ID after the combobox has re-rendered.
|
||||||
const newSelectedBadge = this.get('grantableBadges')[0];
|
const newSelectedBadge = this.get('grantableBadges')[0];
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div class='admin-container user-badges'>
|
<div class='admin-container user-badges'>
|
||||||
<h2>{{i18n 'admin.badges.grant_badge'}}</h2>
|
<h2>{{i18n 'admin.badges.grant_badge'}}</h2>
|
||||||
<br>
|
<br>
|
||||||
{{#if noBadges}}
|
{{#if noGrantableBadges}}
|
||||||
<p>{{i18n 'admin.badges.no_badges'}}</p>
|
<p>{{i18n 'admin.badges.no_badges'}}</p>
|
||||||
{{else}}
|
{{else}}
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
@ -22,7 +22,7 @@
|
|||||||
<label>{{i18n 'admin.badges.reason'}}</label>
|
<label>{{i18n 'admin.badges.reason'}}</label>
|
||||||
{{input type="text" value=badgeReason}}<br><small>{{i18n 'admin.badges.reason_help'}}</small>
|
{{input type="text" value=badgeReason}}<br><small>{{i18n 'admin.badges.reason_help'}}</small>
|
||||||
</label>
|
</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>
|
</form>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -60,9 +60,9 @@
|
|||||||
//= require ./discourse/models/user-action
|
//= require ./discourse/models/user-action
|
||||||
//= require ./discourse/models/draft
|
//= require ./discourse/models/draft
|
||||||
//= require ./discourse/models/composer
|
//= require ./discourse/models/composer
|
||||||
|
//= require ./discourse/models/user-badge
|
||||||
//= require_tree ./discourse/mixins
|
//= require_tree ./discourse/mixins
|
||||||
//= require ./discourse/models/invite
|
//= require ./discourse/models/invite
|
||||||
//= require ./discourse/models/user-badge
|
|
||||||
//= require ./discourse/controllers/discovery-sortable
|
//= require ./discourse/controllers/discovery-sortable
|
||||||
//= require ./discourse/controllers/navigation/default
|
//= require ./discourse/controllers/navigation/default
|
||||||
//= require ./discourse/components/edit-category-panel
|
//= 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');
|
this.send('changeOwner');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
grantBadge(post) {
|
||||||
|
this.set("selectedPostIds", [post.id]);
|
||||||
|
this.send('showGrantBadgeModal');
|
||||||
|
},
|
||||||
|
|
||||||
toggleParticipant(user) {
|
toggleParticipant(user) {
|
||||||
this.get("model.postStream")
|
this.get("model.postStream")
|
||||||
.toggleParticipant(user.get("username"))
|
.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');
|
this.controllerFor('modal').set('modalClass', 'history-modal');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showGrantBadgeModal() {
|
||||||
|
showModal('grant-badge', { model: this.modelFor('topic'), title: 'admin.badges.grant_badge' });
|
||||||
|
},
|
||||||
|
|
||||||
showRawEmail(model) {
|
showRawEmail(model) {
|
||||||
showModal('raw-email', { model });
|
showModal('raw-email', { model });
|
||||||
this.controllerFor('raw_email').loadRawEmail(model.get("id"));
|
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")
|
togglePostType=(action "togglePostType")
|
||||||
rebakePost=(action "rebakePost")
|
rebakePost=(action "rebakePost")
|
||||||
changePostOwner=(action "changePostOwner")
|
changePostOwner=(action "changePostOwner")
|
||||||
|
grantBadge=(action "grantBadge")
|
||||||
unhidePost=(action "unhidePost")
|
unhidePost=(action "unhidePost")
|
||||||
replyToPost=(action "replyToPost")
|
replyToPost=(action "replyToPost")
|
||||||
toggleWiki=(action "toggleWiki")
|
toggleWiki=(action "toggleWiki")
|
||||||
|
@ -64,6 +64,13 @@ export function buildManageButtons(attrs, currentUser) {
|
|||||||
action: 'changePostOwner',
|
action: 'changePostOwner',
|
||||||
className: 'change-owner'
|
className: 'change-owner'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
contents.push({
|
||||||
|
icon: 'certificate',
|
||||||
|
label: 'post.controls.grant_badge',
|
||||||
|
action: 'grantBadge',
|
||||||
|
className: 'grant-badge'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attrs.canManage || attrs.canWiki) {
|
if (attrs.canManage || attrs.canWiki) {
|
||||||
|
@ -219,6 +219,10 @@ class Badge < ActiveRecord::Base
|
|||||||
Slug.for(self.display_name, '-')
|
Slug.for(self.display_name, '-')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def manually_grantable?
|
||||||
|
query.blank? && !system?
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def ensure_not_system
|
def ensure_not_system
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
class BadgeSerializer < ApplicationSerializer
|
class BadgeSerializer < ApplicationSerializer
|
||||||
attributes :id, :name, :description, :grant_count, :allow_title,
|
attributes :id, :name, :description, :grant_count, :allow_title,
|
||||||
:multiple_grant, :icon, :image, :listable, :enabled, :badge_grouping_id,
|
: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
|
has_one :badge_type
|
||||||
|
|
||||||
|
@ -1979,6 +1979,7 @@ en:
|
|||||||
rebake: "Rebuild HTML"
|
rebake: "Rebuild HTML"
|
||||||
unhide: "Unhide"
|
unhide: "Unhide"
|
||||||
change_owner: "Change Ownership"
|
change_owner: "Change Ownership"
|
||||||
|
grant_badge: "Grant Badge"
|
||||||
|
|
||||||
actions:
|
actions:
|
||||||
flag: 'Flag'
|
flag: 'Flag'
|
||||||
@ -2487,6 +2488,7 @@ en:
|
|||||||
other: "%{count} granted"
|
other: "%{count} granted"
|
||||||
select_badge_for_title: Select a badge to use as your title
|
select_badge_for_title: Select a badge to use as your title
|
||||||
none: "(none)"
|
none: "(none)"
|
||||||
|
successfully_granted: "Successfully granted %{badge} to %{username}"
|
||||||
badge_grouping:
|
badge_grouping:
|
||||||
getting_started:
|
getting_started:
|
||||||
name: Getting Started
|
name: Getting Started
|
||||||
|
@ -61,4 +61,23 @@ describe Badge do
|
|||||||
expect(b.grant_count).to eq(1)
|
expect(b.grant_count).to eq(1)
|
||||||
end
|
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
|
end
|
||||||
|
@ -9,11 +9,17 @@ moduleFor('controller:admin-user-badges', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test("grantableBadges", function(assert) {
|
QUnit.test("grantableBadges", function(assert) {
|
||||||
const badgeFirst = Badge.create({id: 3, name: "A Badge", enabled: true});
|
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});
|
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});
|
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});
|
const badgeDisabled = Badge.create({ id: 4, name: "Disabled Badge", enabled: false, manually_grantable: true });
|
||||||
const controller = this.subject({ model: [], badges: [badgeLast, badgeFirst, badgeMiddle, badgeDisabled] });
|
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 sortedNames = [badgeFirst.name, badgeMiddle.name, badgeLast.name];
|
||||||
const badgeNames = controller.get('grantableBadges').map(function(badge) {
|
const badgeNames = controller.get('grantableBadges').map(function(badge) {
|
||||||
return badge.name;
|
return badge.name;
|
||||||
|
38
test/javascripts/mixins/grant-badge-controller-test.js.es6
Normal file
38
test/javascripts/mixins/grant-badge-controller-test.js.es6
Normal 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'));
|
||||||
|
});
|
Reference in New Issue
Block a user