Interface for granting/revoking badges from admin user page.

This commit is contained in:
Vikhyat Korrapati 2014-03-19 19:57:21 +05:30
parent 8163fcade7
commit 0f9ea25010
10 changed files with 244 additions and 13 deletions

View File

@ -0,0 +1,52 @@
/**
This controller supports the interface for granting and revoking badges from
individual users.
@class AdminUserBadgesController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
Discourse.AdminUserBadgesController = Ember.ArrayController.extend({
needs: ["adminUser"],
user: Em.computed.alias('controllers.adminUser'),
sortProperties: ['granted_at'],
sortAscending: false,
actions: {
/**
Grant the selected badge to the user.
@method grantBadge
@param {Integer} badgeId id of the badge we want to grant.
**/
grantBadge: function(badgeId) {
var self = this;
Discourse.UserBadge.grant(badgeId, this.get('user.username')).then(function(userBadge) {
self.pushObject(userBadge);
}, function() {
// Failure
bootbox.alert(I18n.t('generic_error'));
});
},
/**
Revoke the selected userBadge.
@method revokeBadge
@param {Discourse.UserBadge} userBadge the `Discourse.UserBadge` instance that needs to be revoked.
**/
revokeBadge: function(userBadge) {
var self = this;
return bootbox.confirm(I18n.t("admin.badges.revoke_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
if (result) {
userBadge.revoke().then(function() {
self.get('model').removeObject(userBadge);
});
}
});
}
}
});

View File

@ -47,6 +47,7 @@ Discourse.Route.buildRoutes(function() {
this.resource('adminUsers', { path: '/users' }, function() {
this.resource('adminUser', { path: '/:username' }, function() {
this.route('badges');
this.route('leaderRequirements', { path: '/leader_requirements' });
});
this.resource('adminUsersList', { path: '/list' }, function() {

View File

@ -0,0 +1,31 @@
/**
Shows all of the badges that have been granted to a user, and allow granting and
revoking badges.
@class AdminUserBadgesRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminUserBadgesRoute = Discourse.Route.extend({
model: function() {
var username = this.controllerFor('adminUser').get('username');
return Discourse.UserBadge.findByUsername(username);
},
setupController: function(controller, model) {
// Find all badges.
controller.set('loading', true);
Discourse.Badge.findAll().then(function(badges) {
controller.set('badges', badges);
if (badges.length > 0) {
controller.set('selectedBadgeId', badges[0].get('id'));
} else {
controller.set('noBadges', true);
}
controller.set('loading', false);
});
// Set the model.
controller.set('model', model);
}
});

View File

@ -0,0 +1,61 @@
<div class='admin-controls'>
<div class='span15'>
<ul class='nav nav-pills'>
<li>{{#link-to 'adminUser' user}}<i class="fa fa-caret-left"></i> &nbsp;{{user.username}}{{/link-to}}</li>
</ul>
</div>
</div>
{{#if loading}}
<div class='spinner'>{{i18n loading}}</div>
{{else}}
<div class='admin-container user-badges'>
<h2>{{i18n admin.badges.grant_badge}}</h2>
{{#if noBadges}}
<p>{{i18n admin.badges.no_badges}}</p>
{{else}}
<br>
{{combobox valueAttribute="id" value=controller.selectedBadgeId content=controller.badges}}
<button class='btn btn-primary' {{action grantBadge controller.selectedBadgeId}}>{{i18n admin.badges.grant}}</button>
{{/if}}
<br>
<br>
<h2>{{i18n admin.badges.granted_badges}}</h2>
<br>
<table>
<tr>
<th>{{i18n admin.badges.name}}</th>
<th>{{i18n admin.badges.badge_type}}</th>
<th>{{i18n admin.badges.granted_by}}</th>
<th>{{i18n admin.badges.granted_at}}</th>
<th></th>
</tr>
{{#each}}
<tr>
<td>{{badge.displayName}}</td>
<td>{{badge.badge_type.name}}</td>
<td>
{{#link-to 'adminUser' badge.granted_by}}
{{avatar granted_by imageSize="tiny"}}
{{granted_by.username}}
{{/link-to}}
</td>
<td>{{unboundAgeWithTooltip granted_at}}</td>
<td>
<button class='btn' {{action revokeBadge this}}>{{i18n admin.badges.revoke}}</button>
</td>
</tr>
{{else}}
<tr>
<td colspan="5">
<p>{{i18n admin.badges.no_user_badges name=user.username}}</p>
</td>
</tr>
{{/each}}
</table>
</div>
{{/if}}

View File

@ -77,6 +77,18 @@
</div>
</div>
{{#if showBadges}}
<div class='display-row'>
<div class='field'>{{i18n admin.badges.title}}</div>
<div class='value'>
TODO featured badges
</div>
<div class='controls'>
{{#link-to 'adminUser.badges' this class="btn"}}{{i18n admin.badges.edit_badges}}{{/link-to}}
</div>
</div>
{{/if}}
</section>
@ -336,12 +348,6 @@
</div>
</section>
{{#if showBadges}}
<section class='details'>
<h1>{{i18n admin.badges.title}}</h1>
</section>
{{/if}}
<section>
<hr/>
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}}>

View File

@ -7,6 +7,17 @@
@module Discourse
**/
Discourse.UserBadge = Discourse.Model.extend({
/**
Revoke this badge.
@method revoke
@returns {Promise} a promise that resolves when the badge has been revoked.
**/
revoke: function() {
return Discourse.ajax("/user_badges/" + this.get('id'), {
type: "DELETE"
});
}
});
Discourse.UserBadge.reopenClass({
@ -19,14 +30,15 @@ Discourse.UserBadge.reopenClass({
**/
createFromJson: function(json) {
// Create User objects.
if (json.users === undefined) { json.users = []; }
var users = {};
json.users.forEach(function(userJson) {
users[userJson.id] = Discourse.User.create(userJson);
});
// Create the badges.
if (json.badges === undefined) { json.badges = []; }
var badges = {};
Discourse.Badge.createFromJson(json).forEach(function(badge) {
badges[badge.get('id')] = badge;
});
@ -53,5 +65,37 @@ Discourse.UserBadge.reopenClass({
} else {
return userBadges;
}
},
/**
Find all badges for a given username.
@method findByUsername
@returns {Promise} a promise that resolves to an array of `Discourse.UserBadge`.
**/
findByUsername: function(username) {
return Discourse.ajax("/user_badges.json?username=" + username).then(function(json) {
return Discourse.UserBadge.createFromJson(json);
});
},
/**
Grant the badge having id `badgeId` to the user identified by `username`.
@method grant
@param {Integer} badgeId id of the badge to be granted.
@param {String} username username of the user to be granted the badge.
@returns {Promise} a promise that resolves to an instance of `Discourse.UserBadge`.
**/
grant: function(badgeId, username) {
return Discourse.ajax("/user_badges", {
type: "POST",
data: {
username: username,
badge_id: badgeId
}
}).then(function(json) {
return Discourse.UserBadge.createFromJson(json);
});
}
});

View File

@ -169,6 +169,9 @@ class Admin::UsersController < Admin::AdminController
end
end
def badges
end
def leader_requirements
end

View File

@ -1678,9 +1678,19 @@ en:
display_name: Display Name
description: Description
badge_type: Badge Type
granted_by: Granted By
granted_at: Granted At
save: Save
delete: Delete
delete_confirm: Are you sure you want to delete this badge?
revoke: Revoke
revoke_confirm: Are you sure you want to revoke this badge?
edit_badges: Edit Badges
grant_badge: Grant Badge
granted_badges: Granted Badges
grant: Grant
no_user_badges: "%{name} has not been granted any badges."
no_badges: There are no badges that can be granted.
lightbox:
download: "download"

View File

@ -61,6 +61,7 @@ Discourse::Application.routes.draw do
put "unblock"
put "trust_level"
put "primary_group"
get "badges"
get "leader_requirements"
end

View File

@ -1,9 +1,10 @@
module("Discourse.UserBadge");
test('createFromJson single', function() {
var json = {"badges":[{"id":874,"name":"Badge 2","description":null,"badge_type_id":7}],"badge_types":[{"id":7,"name":"Silver 2","color_hexcode":"#c0c0c0"}],"users":[{"id":13470,"username":"anne3","avatar_template":"//www.gravatar.com/avatar/a4151b1fd72089c54e2374565a87da7f.png?s={size}\u0026r=pg\u0026d=identicon"}],"user_badge":{"id":665,"granted_at":"2014-03-09T20:30:01.190-04:00","badge_id":874,"granted_by_id":13470}};
var singleBadgeJson = {"badges":[{"id":874,"name":"Badge 2","description":null,"badge_type_id":7}],"badge_types":[{"id":7,"name":"Silver 2","color_hexcode":"#c0c0c0"}],"users":[{"id":13470,"username":"anne3","avatar_template":"//www.gravatar.com/avatar/a4151b1fd72089c54e2374565a87da7f.png?s={size}\u0026r=pg\u0026d=identicon"}],"user_badge":{"id":665,"granted_at":"2014-03-09T20:30:01.190-04:00","badge_id":874,"granted_by_id":13470}},
multipleBadgesJson = {"badges":[{"id":880,"name":"Badge 8","description":null,"badge_type_id":13}],"badge_types":[{"id":13,"name":"Silver 8","color_hexcode":"#c0c0c0"}],"users":[],"user_badges":[{"id":668,"granted_at":"2014-03-09T20:30:01.420-04:00","badge_id":880,"granted_by_id":null}]};
var userBadge = Discourse.UserBadge.createFromJson(json);
test('createFromJson single', function() {
var userBadge = Discourse.UserBadge.createFromJson(singleBadgeJson);
ok(!Array.isArray(userBadge), "does not return an array");
equal(userBadge.get('badge.name'), "Badge 2", "badge reference is set");
equal(userBadge.get('badge.badge_type.name'), "Silver 2", "badge.badge_type reference is set");
@ -11,9 +12,30 @@ test('createFromJson single', function() {
});
test('createFromJson array', function() {
var json = {"badges":[{"id":880,"name":"Badge 8","description":null,"badge_type_id":13}],"badge_types":[{"id":13,"name":"Silver 8","color_hexcode":"#c0c0c0"}],"users":[],"user_badges":[{"id":668,"granted_at":"2014-03-09T20:30:01.420-04:00","badge_id":880,"granted_by_id":null}]};
var userBadges = Discourse.UserBadge.createFromJson(json);
var userBadges = Discourse.UserBadge.createFromJson(multipleBadgesJson);
ok(Array.isArray(userBadges), "returns an array");
equal(userBadges[0].get('granted_by'), null, "granted_by reference is not set when null");
});
test('findByUsername', function() {
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(multipleBadgesJson));
Discourse.UserBadge.findByUsername("anne3").then(function(badges) {
ok(Array.isArray(badges), "returns an array");
});
ok(Discourse.ajax.calledOnce, "makes an AJAX call");
});
test('grant', function() {
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(singleBadgeJson));
Discourse.UserBadge.grant(1, "username").then(function(userBadge) {
ok(!Array.isArray(userBadge), "does not return an array");
});
ok(Discourse.ajax.calledOnce, "makes an AJAX call");
});
test('revoke', function() {
this.stub(Discourse, 'ajax');
var userBadge = Discourse.UserBadge.create({id: 1});
userBadge.revoke();
ok(Discourse.ajax.calledOnce, "makes an AJAX call");
});