Initial badge system implementation.
This commit is contained in:
parent
9eb3958374
commit
9b26c8584e
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
This is the itemController for `Discourse.AdminBadgesController`. Its main purpose
|
||||||
|
is to indicate which badge was selected.
|
||||||
|
|
||||||
|
@class AdminBadgeController
|
||||||
|
@extends Discourse.ObjectController
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
|
||||||
|
Discourse.AdminBadgeController = Discourse.ObjectController.extend({
|
||||||
|
/**
|
||||||
|
Whether this badge has been selected.
|
||||||
|
|
||||||
|
@property selected
|
||||||
|
@type {Boolean}
|
||||||
|
**/
|
||||||
|
selected: Discourse.computed.propertyEqual('model.name', 'parentController.selectedItem.name')
|
||||||
|
});
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
This controller supports the interface for dealing with badges.
|
||||||
|
|
||||||
|
@class AdminBadgesController
|
||||||
|
@extends Ember.ArrayController
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.AdminBadgesController = Ember.ArrayController.extend({
|
||||||
|
itemController: 'adminBadge',
|
||||||
|
|
||||||
|
/**
|
||||||
|
Show the displayName only if it is different from the name.
|
||||||
|
|
||||||
|
@property showDisplayName
|
||||||
|
@type {Boolean}
|
||||||
|
**/
|
||||||
|
showDisplayName: Discourse.computed.propertyNotEqual('selectedItem.name', 'selectedItem.displayName'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
We don't allow setting a description if a translation for the given badge name
|
||||||
|
exists.
|
||||||
|
|
||||||
|
@property canEditDescription
|
||||||
|
@type {Boolean}
|
||||||
|
**/
|
||||||
|
canEditDescription: Em.computed.none('selectedItem.translatedDescription'),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a new badge and select it.
|
||||||
|
|
||||||
|
@method newBadge
|
||||||
|
**/
|
||||||
|
newBadge: function() {
|
||||||
|
var badge = Discourse.Badge.create({
|
||||||
|
name: I18n.t('admin.badges.new_badge')
|
||||||
|
});
|
||||||
|
this.pushObject(badge);
|
||||||
|
this.send('selectBadge', badge);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Select a particular badge.
|
||||||
|
|
||||||
|
@method selectBadge
|
||||||
|
@param {Discourse.Badge} badge The badge to be selected
|
||||||
|
**/
|
||||||
|
selectBadge: function(badge) {
|
||||||
|
this.set('selectedItem', badge);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save the selected badge.
|
||||||
|
|
||||||
|
@method save
|
||||||
|
**/
|
||||||
|
save: function() {
|
||||||
|
var badge = this.get('selectedItem');
|
||||||
|
badge.set('disableSave', true);
|
||||||
|
badge.save().then(function() {
|
||||||
|
badge.set('disableSave', false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Confirm before destroying the selected badge.
|
||||||
|
|
||||||
|
@method destroy
|
||||||
|
**/
|
||||||
|
destroy: function() {
|
||||||
|
var self = this;
|
||||||
|
return bootbox.confirm(I18n.t("admin.badges.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
|
||||||
|
if (result) {
|
||||||
|
var selected = self.get('selectedItem');
|
||||||
|
selected.destroy().then(function() {
|
||||||
|
// Success.
|
||||||
|
self.set('selectedItem', null);
|
||||||
|
self.get('model').removeObject(selected);
|
||||||
|
}, function() {
|
||||||
|
// Failure.
|
||||||
|
bootbox.alert(I18n.t('generic_error'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -24,6 +24,10 @@ Discourse.AdminUserIndexController = Discourse.ObjectController.extend({
|
||||||
return Discourse.SiteSettings.must_approve_users;
|
return Discourse.SiteSettings.must_approve_users;
|
||||||
}.property(),
|
}.property(),
|
||||||
|
|
||||||
|
showBadges: function() {
|
||||||
|
return Discourse.SiteSettings.enable_badges;
|
||||||
|
}.property(),
|
||||||
|
|
||||||
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
|
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
Discourse.AdminBadgesRoute = Discourse.Route.extend({
|
||||||
|
|
||||||
|
model: function() {
|
||||||
|
return Discourse.Badge.findAll();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupController: function(controller, model) {
|
||||||
|
Discourse.ajax('/admin/badges/types').then(function(json) {
|
||||||
|
controller.set('badgeTypes', json.badge_types);
|
||||||
|
});
|
||||||
|
controller.set('model', model);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
|
@ -57,5 +57,7 @@ Discourse.Route.buildRoutes(function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route('badges');
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<li>{{#link-to 'admin.customize'}}{{i18n admin.customize.title}}{{/link-to}}</li>
|
<li>{{#link-to 'admin.customize'}}{{i18n admin.customize.title}}{{/link-to}}</li>
|
||||||
<li>{{#link-to 'admin.api'}}{{i18n admin.api.title}}{{/link-to}}</li>
|
<li>{{#link-to 'admin.api'}}{{i18n admin.api.title}}{{/link-to}}</li>
|
||||||
<li>{{#link-to 'admin.backups'}}{{i18n admin.backups.title}}{{/link-to}}</li>
|
<li>{{#link-to 'admin.backups'}}{{i18n admin.backups.title}}{{/link-to}}</li>
|
||||||
|
<li>{{#link-to 'admin.badges'}}{{i18n admin.badges.title}}{{/link-to}}</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
<div class="badges">
|
||||||
|
|
||||||
|
<div class='content-list span6'>
|
||||||
|
<h3>{{i18n admin.badges.title}}</h3>
|
||||||
|
<ul>
|
||||||
|
{{#each}}
|
||||||
|
<li>
|
||||||
|
<a {{action selectBadge this}} {{bind-attr class="selected:active"}}>
|
||||||
|
{{displayName}}
|
||||||
|
{{#if newBadge}}
|
||||||
|
(*)
|
||||||
|
{{/if}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
<button {{action newBadge}} class='btn'>{{i18n admin.badges.new}}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if selectedItem}}
|
||||||
|
{{#with selectedItem}}
|
||||||
|
<div class='current-badge span12'>
|
||||||
|
<form class="form-horizontal">
|
||||||
|
<div>
|
||||||
|
<label for="name">{{i18n admin.badges.name}}</label>
|
||||||
|
{{input type="text" name="name" value=name}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if controller.showDisplayName}}
|
||||||
|
<div>
|
||||||
|
<strong>{{i18n admin.badges.display_name}}</strong>
|
||||||
|
{{displayName}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="badge_type_id">{{i18n admin.badges.badge_type}}</label>
|
||||||
|
{{view Ember.Select name="badge_type_id" value=badge_type_id
|
||||||
|
content=controller.badgeTypes
|
||||||
|
optionValuePath="content.id"
|
||||||
|
optionLabelPath="content.name"}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="description">{{i18n admin.badges.description}}</label>
|
||||||
|
{{#if controller.canEditDescription}}
|
||||||
|
{{textarea name="description" value=description}}
|
||||||
|
{{else}}
|
||||||
|
{{textarea name="description" value=translatedDescription disabled=true}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class='buttons'>
|
||||||
|
<button {{action save}} {{bind-attr disabled=disableSave}} class='btn btn-primary'>{{i18n admin.badges.save}}</button>
|
||||||
|
<a {{action destroy}} class='delete-link'>{{i18n admin.badges.delete}}</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{/with}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
</div>
|
|
@ -336,6 +336,12 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{{#if showBadges}}
|
||||||
|
<section class='details'>
|
||||||
|
<h1>{{i18n admin.badges.title}}</h1>
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<hr/>
|
<hr/>
|
||||||
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}}>
|
<button {{bind-attr class=":btn :btn-danger :pull-right deleteForbidden:hidden"}} {{action destroy target="content"}} {{bind-attr disabled="deleteForbidden"}}>
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
/**
|
||||||
|
A data model representing a badge on Discourse
|
||||||
|
|
||||||
|
@class Badge
|
||||||
|
@extends Discourse.Model
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.Badge = Discourse.Model.extend({
|
||||||
|
/**
|
||||||
|
Is this a new badge?
|
||||||
|
|
||||||
|
@property newBadge
|
||||||
|
@type {String}
|
||||||
|
**/
|
||||||
|
newBadge: Em.computed.none('id'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
@private
|
||||||
|
|
||||||
|
The name key to use for fetching i18n translations.
|
||||||
|
|
||||||
|
@property i18nNameKey
|
||||||
|
@type {String}
|
||||||
|
**/
|
||||||
|
i18nNameKey: function() {
|
||||||
|
return this.get('name').toLowerCase().replace(/\s/g, '_');
|
||||||
|
}.property('name'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
The display name of this badge. Attempts to use a translation and falls back to
|
||||||
|
the actual name.
|
||||||
|
|
||||||
|
@property displayName
|
||||||
|
@type {String}
|
||||||
|
**/
|
||||||
|
displayName: function() {
|
||||||
|
var i18nKey = "badges." + this.get('i18nNameKey') + ".name";
|
||||||
|
return I18n.t(i18nKey, {defaultValue: this.get('name')});
|
||||||
|
}.property('name', 'i18nNameKey'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
The i18n translated description for this badge. `null` if no translation exists.
|
||||||
|
|
||||||
|
@property translatedDescription
|
||||||
|
@type {String}
|
||||||
|
**/
|
||||||
|
translatedDescription: function() {
|
||||||
|
var i18nKey = "badges." + this.get('i18nNameKey') + ".description",
|
||||||
|
translation = I18n.t(i18nKey);
|
||||||
|
if (translation.match(new RegExp(i18nKey))) {
|
||||||
|
translation = null;
|
||||||
|
}
|
||||||
|
return translation;
|
||||||
|
}.property('i18nNameKey'),
|
||||||
|
|
||||||
|
/**
|
||||||
|
Update this badge with the response returned by the server on save.
|
||||||
|
|
||||||
|
@method updateFromJson
|
||||||
|
@param {Object} json The JSON response returned by the server
|
||||||
|
**/
|
||||||
|
updateFromJson: function(json) {
|
||||||
|
var self = this;
|
||||||
|
Object.keys(json.badge).forEach(function(key) {
|
||||||
|
self.set(key, json.badge[key]);
|
||||||
|
});
|
||||||
|
json.badge_types.forEach(function(badgeType) {
|
||||||
|
if (badgeType.id === self.get('badge_type_id')) {
|
||||||
|
self.set('badge_type', Object.create(badgeType));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save and update the badge from the server's response.
|
||||||
|
|
||||||
|
@method save
|
||||||
|
@returns {Promise} A promise that resolves to the updated `Discourse.Badge`
|
||||||
|
**/
|
||||||
|
save: function() {
|
||||||
|
var url = "/admin/badges",
|
||||||
|
requestType = "POST",
|
||||||
|
self = this;
|
||||||
|
if (!this.get('newBadge')) {
|
||||||
|
// We are updating an existing badge.
|
||||||
|
url += "/" + this.get('id');
|
||||||
|
requestType = "PUT";
|
||||||
|
}
|
||||||
|
return Discourse.ajax(url, {
|
||||||
|
type: requestType,
|
||||||
|
data: {
|
||||||
|
name: this.get('name'),
|
||||||
|
description: this.get('description'),
|
||||||
|
badge_type_id: this.get('badge_type_id')
|
||||||
|
}
|
||||||
|
}).then(function(json) {
|
||||||
|
self.updateFromJson(json);
|
||||||
|
return self;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Destroy the badge.
|
||||||
|
|
||||||
|
@method destroy
|
||||||
|
@returns {Promise} A promise that resolves to the server response
|
||||||
|
**/
|
||||||
|
destroy: function() {
|
||||||
|
if (this.get('newBadge')) return Ember.RSVP.resolve();
|
||||||
|
return Discourse.ajax("/admin/badges/" + this.get('id'), {
|
||||||
|
type: "DELETE"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Discourse.Badge.reopenClass({
|
||||||
|
/**
|
||||||
|
Create `Discourse.Badge` instances from the server JSON response.
|
||||||
|
|
||||||
|
@method createFromJson
|
||||||
|
@param {Object} json The JSON returned by the server
|
||||||
|
@returns Array or instance of `Discourse.Badge` depending on the input JSON
|
||||||
|
**/
|
||||||
|
createFromJson: function(json) {
|
||||||
|
// Create BadgeType objects.
|
||||||
|
var badgeTypes = {};
|
||||||
|
if ('badge_types' in json) {
|
||||||
|
json.badge_types.forEach(function(badgeTypeJson) {
|
||||||
|
badgeTypes[badgeTypeJson.id] = Ember.Object.create(badgeTypeJson);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Badge objects.
|
||||||
|
var badges = [];
|
||||||
|
if ("badge" in json) {
|
||||||
|
badges = [json.badge];
|
||||||
|
} else {
|
||||||
|
badges = json.badges;
|
||||||
|
}
|
||||||
|
badges = badges.map(function(badgeJson) {
|
||||||
|
var badge = Discourse.Badge.create(badgeJson);
|
||||||
|
badge.set('badge_type', badgeTypes[badge.get('badge_type_id')]);
|
||||||
|
return badge;
|
||||||
|
});
|
||||||
|
if ("badge" in json) {
|
||||||
|
return badges[0];
|
||||||
|
} else {
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
Find all `Discourse.Badge` instances that have been defined.
|
||||||
|
|
||||||
|
@method findAll
|
||||||
|
@returns {Promise} a promise that resolves to an array of `Discourse.Badge`
|
||||||
|
**/
|
||||||
|
findAll: function() {
|
||||||
|
return Discourse.ajax('/admin/badges').then(function(badgesJson) {
|
||||||
|
return Discourse.Badge.createFromJson(badgesJson);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
A data model representing a user badge grant on Discourse
|
||||||
|
|
||||||
|
@class UserBadge
|
||||||
|
@extends Discourse.Model
|
||||||
|
@namespace Discourse
|
||||||
|
@module Discourse
|
||||||
|
**/
|
||||||
|
Discourse.UserBadge = Discourse.Model.extend({
|
||||||
|
});
|
||||||
|
|
||||||
|
Discourse.UserBadge.reopenClass({
|
||||||
|
/**
|
||||||
|
Create `Discourse.UserBadge` instances from the server JSON response.
|
||||||
|
|
||||||
|
@method createFromJson
|
||||||
|
@param {Object} json The JSON returned by the server
|
||||||
|
@returns Array or instance of `Discourse.UserBadge` depending on the input JSON
|
||||||
|
**/
|
||||||
|
createFromJson: function(json) {
|
||||||
|
// Create User objects.
|
||||||
|
var users = {};
|
||||||
|
json.users.forEach(function(userJson) {
|
||||||
|
users[userJson.id] = Discourse.User.create(userJson);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the badges.
|
||||||
|
var badges = {};
|
||||||
|
|
||||||
|
Discourse.Badge.createFromJson(json).forEach(function(badge) {
|
||||||
|
badges[badge.get('id')] = badge;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create UserBadge object(s).
|
||||||
|
var userBadges = [];
|
||||||
|
if ("user_badge" in json) {
|
||||||
|
userBadges = [json.user_badge];
|
||||||
|
} else {
|
||||||
|
userBadges = json.user_badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
userBadges = userBadges.map(function(userBadgeJson) {
|
||||||
|
var userBadge = Discourse.UserBadge.create(userBadgeJson);
|
||||||
|
userBadge.set('badge', badges[userBadge.get('badge_id')]);
|
||||||
|
if (userBadge.get('granted_by_id')) {
|
||||||
|
userBadge.set('granted_by', users[userBadge.get('granted_by_id')]);
|
||||||
|
}
|
||||||
|
return userBadge;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("user_badge" in json) {
|
||||||
|
return userBadges[0];
|
||||||
|
} else {
|
||||||
|
return userBadges;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -285,6 +285,30 @@ section.details {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Badges area
|
||||||
|
.badges {
|
||||||
|
.content-list ul {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-badge {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-horizontal {
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
& > div {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.delete-link {
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Customise area
|
// Customise area
|
||||||
.customize {
|
.customize {
|
||||||
.nav.nav-pills {
|
.nav.nav-pills {
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
class Admin::BadgesController < Admin::AdminController
|
||||||
|
def index
|
||||||
|
badges = Badge.all.to_a
|
||||||
|
render_serialized(badges, BadgeSerializer, root: "badges")
|
||||||
|
end
|
||||||
|
|
||||||
|
def badge_types
|
||||||
|
badge_types = BadgeType.all.to_a
|
||||||
|
render_serialized(badge_types, BadgeTypeSerializer, root: "badge_types")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
badge = Badge.new
|
||||||
|
update_badge_from_params(badge)
|
||||||
|
badge.save!
|
||||||
|
render_serialized(badge, BadgeSerializer, root: "badge")
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
badge = find_badge
|
||||||
|
update_badge_from_params(badge)
|
||||||
|
badge.save!
|
||||||
|
render_serialized(badge, BadgeSerializer, root: "badge")
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
find_badge.destroy
|
||||||
|
render nothing: true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def find_badge
|
||||||
|
params.require(:id)
|
||||||
|
Badge.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_badge_from_params(badge)
|
||||||
|
params.permit(:name, :description, :badge_type_id)
|
||||||
|
badge.name = params[:name]
|
||||||
|
badge.description = params[:description]
|
||||||
|
badge.badge_type = BadgeType.find(params[:badge_type_id])
|
||||||
|
badge
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,58 @@
|
||||||
|
class UserBadgesController < ApplicationController
|
||||||
|
def index
|
||||||
|
params.require(:username)
|
||||||
|
user = fetch_user_from_params
|
||||||
|
render json: user.user_badges
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
params.require(:username)
|
||||||
|
user = fetch_user_from_params
|
||||||
|
|
||||||
|
unless can_assign_badge_to_user?(user)
|
||||||
|
render json: failed_json, status: 403
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
badge = fetch_badge_from_params
|
||||||
|
user_badge = BadgeGranter.grant(badge, user, granted_by: current_user)
|
||||||
|
|
||||||
|
render json: user_badge
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
params.require(:id)
|
||||||
|
user_badge = UserBadge.find(params[:id])
|
||||||
|
|
||||||
|
unless can_assign_badge_to_user?(user_badge.user)
|
||||||
|
render json: failed_json, status: 403
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
BadgeGranter.revoke(user_badge)
|
||||||
|
render json: success_json
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# Get the badge from either the badge name or id specified in the params.
|
||||||
|
def fetch_badge_from_params
|
||||||
|
badge = nil
|
||||||
|
|
||||||
|
params.permit(:badge_name)
|
||||||
|
if params[:badge_name].nil?
|
||||||
|
params.require(:badge_id)
|
||||||
|
badge = Badge.where(id: params[:badge_id]).first
|
||||||
|
else
|
||||||
|
badge = Badge.where(name: params[:badge_name]).first
|
||||||
|
end
|
||||||
|
raise Discourse::NotFound.new if badge.blank?
|
||||||
|
|
||||||
|
badge
|
||||||
|
end
|
||||||
|
|
||||||
|
def can_assign_badge_to_user?(user)
|
||||||
|
master_api_call = current_user.nil? && is_api?
|
||||||
|
master_api_call or guardian.can_grant_badges?(user)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Badge < ActiveRecord::Base
|
||||||
|
belongs_to :badge_type
|
||||||
|
|
||||||
|
validates :name, presence: true, uniqueness: true
|
||||||
|
validates :badge_type, presence: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: badges
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# name :string(255) not null
|
||||||
|
# description :text
|
||||||
|
# badge_type_id :integer not null
|
||||||
|
# grant_count :integer default(0), not null
|
||||||
|
# created_at :datetime
|
||||||
|
# updated_at :datetime
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_badges_on_badge_type_id (badge_type_id)
|
||||||
|
# index_badges_on_name (name) UNIQUE
|
||||||
|
#
|
|
@ -0,0 +1,21 @@
|
||||||
|
class BadgeType < ActiveRecord::Base
|
||||||
|
has_many :badges
|
||||||
|
|
||||||
|
validates :name, presence: true, uniqueness: true
|
||||||
|
validates :color_hexcode, presence: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: badge_types
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# name :string(255) not null
|
||||||
|
# color_hexcode :string(255) not null
|
||||||
|
# created_at :datetime
|
||||||
|
# updated_at :datetime
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_badge_types_on_name (name) UNIQUE
|
||||||
|
#
|
|
@ -21,6 +21,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :user_open_ids, dependent: :destroy
|
has_many :user_open_ids, dependent: :destroy
|
||||||
has_many :user_actions, dependent: :destroy
|
has_many :user_actions, dependent: :destroy
|
||||||
has_many :post_actions, dependent: :destroy
|
has_many :post_actions, dependent: :destroy
|
||||||
|
has_many :user_badges, dependent: :destroy
|
||||||
has_many :email_logs, dependent: :destroy
|
has_many :email_logs, dependent: :destroy
|
||||||
has_many :post_timings
|
has_many :post_timings
|
||||||
has_many :topic_allowed_users, dependent: :destroy
|
has_many :topic_allowed_users, dependent: :destroy
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
class UserBadge < ActiveRecord::Base
|
||||||
|
belongs_to :badge
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :granted_by, class_name: 'User'
|
||||||
|
|
||||||
|
validates :badge_id, presence: true, uniqueness: {scope: :user_id}
|
||||||
|
validates :user_id, presence: true
|
||||||
|
validates :granted_at, presence: true
|
||||||
|
validates :granted_by, presence: true
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: user_badges
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# badge_id :integer not null
|
||||||
|
# user_id :integer not null
|
||||||
|
# granted_at :datetime not null
|
||||||
|
# granted_by_id :integer not null
|
||||||
|
#
|
||||||
|
# Indexes
|
||||||
|
#
|
||||||
|
# index_user_badges_on_badge_id_and_user_id (badge_id,user_id) UNIQUE
|
||||||
|
# index_user_badges_on_user_id (user_id)
|
||||||
|
#
|
|
@ -0,0 +1,5 @@
|
||||||
|
class BadgeSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :name, :description
|
||||||
|
|
||||||
|
has_one :badge_type
|
||||||
|
end
|
|
@ -0,0 +1,3 @@
|
||||||
|
class BadgeTypeSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :name, :color_hexcode
|
||||||
|
end
|
|
@ -0,0 +1,6 @@
|
||||||
|
class UserBadgeSerializer < ApplicationSerializer
|
||||||
|
attributes :id, :granted_at
|
||||||
|
|
||||||
|
has_one :badge
|
||||||
|
has_one :granted_by, serializer: BasicUserSerializer, root: :users
|
||||||
|
end
|
|
@ -0,0 +1,34 @@
|
||||||
|
class BadgeGranter
|
||||||
|
|
||||||
|
def initialize(badge, user, opts={})
|
||||||
|
@badge, @user, @opts = badge, user, opts
|
||||||
|
@granted_by = opts[:granted_by] || Discourse.system_user
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.grant(badge, user, opts={})
|
||||||
|
BadgeGranter.new(badge, user, opts).grant
|
||||||
|
end
|
||||||
|
|
||||||
|
def grant
|
||||||
|
return if @granted_by and !Guardian.new(@granted_by).can_grant_badges?(@user)
|
||||||
|
|
||||||
|
user_badge = nil
|
||||||
|
|
||||||
|
UserBadge.transaction do
|
||||||
|
user_badge = UserBadge.create!(badge: @badge, user: @user,
|
||||||
|
granted_by: @granted_by, granted_at: Time.now)
|
||||||
|
|
||||||
|
Badge.increment_counter 'grant_count', @badge.id
|
||||||
|
end
|
||||||
|
|
||||||
|
user_badge
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.revoke(user_badge)
|
||||||
|
UserBadge.transaction do
|
||||||
|
user_badge.destroy!
|
||||||
|
Badge.decrement_counter 'grant_count', user_badge.badge.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -1666,6 +1666,18 @@ en:
|
||||||
legal: "Legal"
|
legal: "Legal"
|
||||||
uncategorized: 'Uncategorized'
|
uncategorized: 'Uncategorized'
|
||||||
|
|
||||||
|
badges:
|
||||||
|
title: Badges
|
||||||
|
new_badge: New Badge
|
||||||
|
new: New
|
||||||
|
name: Name
|
||||||
|
display_name: Display Name
|
||||||
|
description: Description
|
||||||
|
badge_type: Badge Type
|
||||||
|
save: Save
|
||||||
|
delete: Delete
|
||||||
|
delete_confirm: Are you sure you want to delete this badge?
|
||||||
|
|
||||||
lightbox:
|
lightbox:
|
||||||
download: "download"
|
download: "download"
|
||||||
|
|
||||||
|
@ -1707,3 +1719,8 @@ en:
|
||||||
mark_regular: '<b>m</b> then <b>r</b> Mark topic as regular'
|
mark_regular: '<b>m</b> then <b>r</b> Mark topic as regular'
|
||||||
mark_tracking: '<b>m</b> then <b>t</b> Mark topic as tracking'
|
mark_tracking: '<b>m</b> then <b>t</b> Mark topic as tracking'
|
||||||
mark_watching: '<b>m</b> then <b>w</b> Mark topic as watching'
|
mark_watching: '<b>m</b> then <b>w</b> Mark topic as watching'
|
||||||
|
|
||||||
|
badges:
|
||||||
|
example_badge:
|
||||||
|
name: Example Badge
|
||||||
|
description: This is a generic example badge.
|
||||||
|
|
|
@ -658,6 +658,8 @@ en:
|
||||||
topics_per_period_in_top_page: "How many topics loaded on the top topics page"
|
topics_per_period_in_top_page: "How many topics loaded on the top topics page"
|
||||||
redirect_new_users_to_top_page_duration: "Number of days during which new users are automatically redirect to the top page"
|
redirect_new_users_to_top_page_duration: "Number of days during which new users are automatically redirect to the top page"
|
||||||
|
|
||||||
|
enable_badges: "Enable the badge system (experimental)"
|
||||||
|
|
||||||
allow_index_in_robots_txt: "Site should be indexed by search engines (update robots.txt)"
|
allow_index_in_robots_txt: "Site should be indexed by search engines (update robots.txt)"
|
||||||
email_domains_blacklist: "A pipe-delimited list of email domains that are not allowed. Example: mailinator.com|trashmail.net"
|
email_domains_blacklist: "A pipe-delimited list of email domains that are not allowed. Example: mailinator.com|trashmail.net"
|
||||||
email_domains_whitelist: "A pipe-delimited list of email domains that users may register with. WARNING: Users with email domains other than those listed will not be allowed."
|
email_domains_whitelist: "A pipe-delimited list of email domains that users may register with. WARNING: Users with email domains other than those listed will not be allowed."
|
||||||
|
@ -1396,3 +1398,9 @@ en:
|
||||||
message_to_blank: "message.to is blank"
|
message_to_blank: "message.to is blank"
|
||||||
text_part_body_blank: "text_part.body is blank"
|
text_part_body_blank: "text_part.body is blank"
|
||||||
body_blank: "body is blank"
|
body_blank: "body is blank"
|
||||||
|
|
||||||
|
badges:
|
||||||
|
types:
|
||||||
|
gold: Gold
|
||||||
|
silver: Silver
|
||||||
|
bronze: Bronze
|
||||||
|
|
|
@ -126,6 +126,12 @@ Discourse::Application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :badges, constraints: AdminConstraint.new do
|
||||||
|
collection do
|
||||||
|
get "types" => "badges#badge_types"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
get "memory_stats"=> "diagnostics#memory_stats", constraints: AdminConstraint.new
|
get "memory_stats"=> "diagnostics#memory_stats", constraints: AdminConstraint.new
|
||||||
|
|
||||||
end # admin namespace
|
end # admin namespace
|
||||||
|
@ -235,6 +241,8 @@ Discourse::Application.routes.draw do
|
||||||
end
|
end
|
||||||
resources :user_actions
|
resources :user_actions
|
||||||
|
|
||||||
|
resources :user_badges, only: [:index, :create, :destroy]
|
||||||
|
|
||||||
# We've renamed popular to latest. If people access it we want a permanent redirect.
|
# We've renamed popular to latest. If people access it we want a permanent redirect.
|
||||||
get "popular" => "list#popular_redirect"
|
get "popular" => "list#popular_redirect"
|
||||||
get "popular/more" => "list#popular_redirect"
|
get "popular/more" => "list#popular_redirect"
|
||||||
|
|
|
@ -75,6 +75,9 @@ basic:
|
||||||
default: 50
|
default: 50
|
||||||
redirect_new_users_to_top_page_duration:
|
redirect_new_users_to_top_page_duration:
|
||||||
default: 7
|
default: 7
|
||||||
|
enable_badges:
|
||||||
|
client: true
|
||||||
|
default: false
|
||||||
|
|
||||||
users:
|
users:
|
||||||
enable_sso:
|
enable_sso:
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
BadgeType.seed do |b|
|
||||||
|
b.id = 1
|
||||||
|
b.name = I18n.t('badges.types.gold')
|
||||||
|
b.color_hexcode = "ffd700"
|
||||||
|
end
|
||||||
|
|
||||||
|
BadgeType.seed do |b|
|
||||||
|
b.id = 2
|
||||||
|
b.name = I18n.t('badges.types.silver')
|
||||||
|
b.color_hexcode = "c0c0c0"
|
||||||
|
end
|
||||||
|
|
||||||
|
BadgeType.seed do |b|
|
||||||
|
b.id = 3
|
||||||
|
b.name = I18n.t('badges.types.bronze')
|
||||||
|
b.color_hexcode = "cd7f32"
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
class CreateBadgeTypes < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :badge_types do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.string :color_hexcode, null: false
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :badge_types, [:name], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,14 @@
|
||||||
|
class CreateBadges < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :badges do |t|
|
||||||
|
t.string :name, null: false
|
||||||
|
t.text :description
|
||||||
|
t.integer :badge_type_id, index: true, null: false
|
||||||
|
t.integer :grant_count, null: false, default: 0
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :badges, [:name], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,12 @@
|
||||||
|
class CreateUserBadges < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :user_badges do |t|
|
||||||
|
t.integer :badge_id, null: false
|
||||||
|
t.integer :user_id, index: true, null: false
|
||||||
|
t.datetime :granted_at, null: false
|
||||||
|
t.integer :granted_by_id, null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :user_badges, [:badge_id, :user_id], unique: true
|
||||||
|
end
|
||||||
|
end
|
|
@ -87,6 +87,7 @@ class Guardian
|
||||||
alias :can_move_posts? :can_moderate?
|
alias :can_move_posts? :can_moderate?
|
||||||
alias :can_see_flags? :can_moderate?
|
alias :can_see_flags? :can_moderate?
|
||||||
alias :can_send_activation_email? :can_moderate?
|
alias :can_send_activation_email? :can_moderate?
|
||||||
|
alias :can_grant_badges? :can_moderate?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Admin::BadgesController do
|
||||||
|
it "is a subclass of AdminController" do
|
||||||
|
(Admin::BadgesController < Admin::AdminController).should be_true
|
||||||
|
end
|
||||||
|
|
||||||
|
context "while logged in as an admin" do
|
||||||
|
let!(:user) { log_in(:admin) }
|
||||||
|
let!(:badge) { Fabricate(:badge) }
|
||||||
|
|
||||||
|
context '.index' do
|
||||||
|
it 'returns success' do
|
||||||
|
xhr :get, :index
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns JSON' do
|
||||||
|
xhr :get, :index
|
||||||
|
::JSON.parse(response.body)["badges"].should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '.badge_types' do
|
||||||
|
it 'returns success' do
|
||||||
|
xhr :get, :badge_types
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns JSON' do
|
||||||
|
xhr :get, :badge_types
|
||||||
|
::JSON.parse(response.body)["badge_types"].should be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '.destroy' do
|
||||||
|
it 'returns success' do
|
||||||
|
xhr :delete, :destroy, id: badge.id
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the badge' do
|
||||||
|
xhr :delete, :destroy, id: badge.id
|
||||||
|
Badge.where(id: badge.id).count.should eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context '.update' do
|
||||||
|
it 'returns success' do
|
||||||
|
xhr :put, :update, id: badge.id, name: "123456", badge_type_id: badge.badge_type_id
|
||||||
|
response.should be_success
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates the badge' do
|
||||||
|
xhr :put, :update, id: badge.id, name: "123456", badge_type_id: badge.badge_type_id
|
||||||
|
badge.reload.name.should eq('123456')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,79 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe UserBadgesController do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:badge) { Fabricate(:badge) }
|
||||||
|
|
||||||
|
context 'index' do
|
||||||
|
before do
|
||||||
|
@user_badge = BadgeGranter.grant(badge, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'requires username to be specified' do
|
||||||
|
expect { xhr :get, :index }.to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the user\'s badges' do
|
||||||
|
xhr :get, :index, username: user.username
|
||||||
|
|
||||||
|
response.status.should == 200
|
||||||
|
parsed = JSON.parse(response.body)
|
||||||
|
parsed["user_badges"].length.should == 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'create' do
|
||||||
|
it 'requires username to be specified' do
|
||||||
|
expect { xhr :post, :create, badge_id: badge.id }.to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow regular users to grant badges' do
|
||||||
|
log_in_user Fabricate(:user)
|
||||||
|
xhr :post, :create, badge_id: badge.id, username: user.username
|
||||||
|
response.status.should == 403
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'grants badges from staff' do
|
||||||
|
admin = Fabricate(:admin)
|
||||||
|
log_in_user admin
|
||||||
|
xhr :post, :create, badge_id: badge.id, username: user.username
|
||||||
|
response.status.should == 200
|
||||||
|
user_badge = UserBadge.where(user: user, badge: badge).first
|
||||||
|
user_badge.should be_present
|
||||||
|
user_badge.granted_by.should eq(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not grant badges from regular api calls' do
|
||||||
|
Fabricate(:api_key, user: user)
|
||||||
|
xhr :post, :create, badge_id: badge.id, username: user.username, api_key: user.api_key.key
|
||||||
|
response.status.should == 403
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'grants badges from master api calls' do
|
||||||
|
api_key = Fabricate(:api_key)
|
||||||
|
xhr :post, :create, badge_id: badge.id, username: user.username, api_key: api_key.key
|
||||||
|
response.status.should == 200
|
||||||
|
user_badge = UserBadge.where(user: user, badge: badge).first
|
||||||
|
user_badge.should be_present
|
||||||
|
user_badge.granted_by.should eq(Discourse.system_user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'destroy' do
|
||||||
|
before do
|
||||||
|
@user_badge = BadgeGranter.grant(badge, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'checks that the user is authorized to revoke a badge' do
|
||||||
|
xhr :delete, :destroy, id: @user_badge.id
|
||||||
|
response.status.should == 403
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'revokes the badge' do
|
||||||
|
log_in :admin
|
||||||
|
xhr :delete, :destroy, id: @user_badge.id
|
||||||
|
response.status.should == 200
|
||||||
|
UserBadge.where(id: @user_badge.id).first.should be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,9 @@
|
||||||
|
Fabricator(:badge_type) do
|
||||||
|
name { sequence(:name) {|i| "Silver #{i}" } }
|
||||||
|
color_hexcode "c0c0c0"
|
||||||
|
end
|
||||||
|
|
||||||
|
Fabricator(:badge) do
|
||||||
|
name { sequence(:name) {|i| "Badge #{i}" } }
|
||||||
|
badge_type
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require_dependency 'badge'
|
||||||
|
|
||||||
|
describe Badge do
|
||||||
|
|
||||||
|
it { should belong_to :badge_type }
|
||||||
|
|
||||||
|
context 'validations' do
|
||||||
|
before(:each) { Fabricate(:badge) }
|
||||||
|
|
||||||
|
it { should validate_presence_of :name }
|
||||||
|
it { should validate_presence_of :badge_type }
|
||||||
|
it { should validate_uniqueness_of :name }
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require_dependency 'badge_type'
|
||||||
|
|
||||||
|
describe BadgeType do
|
||||||
|
|
||||||
|
it { should have_many :badges }
|
||||||
|
|
||||||
|
it { should validate_presence_of :name }
|
||||||
|
it { should validate_uniqueness_of :name }
|
||||||
|
it { should validate_presence_of :color_hexcode }
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,20 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
require_dependency 'user_badge'
|
||||||
|
|
||||||
|
describe UserBadge do
|
||||||
|
|
||||||
|
it { should belong_to :badge }
|
||||||
|
it { should belong_to :user }
|
||||||
|
it { should belong_to :granted_by }
|
||||||
|
|
||||||
|
context 'validations' do
|
||||||
|
before(:each) { BadgeGranter.grant(Fabricate(:badge), Fabricate(:user)) }
|
||||||
|
|
||||||
|
it { should validate_presence_of(:badge_id) }
|
||||||
|
it { should validate_presence_of(:user_id) }
|
||||||
|
it { should validate_presence_of(:granted_at) }
|
||||||
|
it { should validate_presence_of(:granted_by) }
|
||||||
|
it { should validate_uniqueness_of(:badge_id).scoped_to(:user_id) }
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -10,6 +10,7 @@ describe User do
|
||||||
it { should have_many(:user_open_ids).dependent(:destroy) }
|
it { should have_many(:user_open_ids).dependent(:destroy) }
|
||||||
it { should have_many(:user_actions).dependent(:destroy) }
|
it { should have_many(:user_actions).dependent(:destroy) }
|
||||||
it { should have_many(:post_actions).dependent(:destroy) }
|
it { should have_many(:post_actions).dependent(:destroy) }
|
||||||
|
it { should have_many(:user_badges).dependent(:destroy) }
|
||||||
it { should have_many(:email_logs).dependent(:destroy) }
|
it { should have_many(:email_logs).dependent(:destroy) }
|
||||||
it { should have_many(:post_timings) }
|
it { should have_many(:post_timings) }
|
||||||
it { should have_many(:topic_allowed_users).dependent(:destroy) }
|
it { should have_many(:topic_allowed_users).dependent(:destroy) }
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe BadgeGranter do
|
||||||
|
|
||||||
|
let(:badge) { Fabricate(:badge) }
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
|
||||||
|
describe 'grant' do
|
||||||
|
|
||||||
|
it 'grants a badge' do
|
||||||
|
user_badge = BadgeGranter.grant(badge, user)
|
||||||
|
user_badge.should be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets granted_at' do
|
||||||
|
time = Time.zone.now
|
||||||
|
Timecop.freeze time
|
||||||
|
|
||||||
|
user_badge = BadgeGranter.grant(badge, user)
|
||||||
|
user_badge.granted_at.should eq(time)
|
||||||
|
|
||||||
|
Timecop.return
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets granted_by if the option is present' do
|
||||||
|
admin = Fabricate(:admin)
|
||||||
|
user_badge = BadgeGranter.grant(badge, user, granted_by: admin)
|
||||||
|
user_badge.granted_by.should eq(admin)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults granted_by to the system user' do
|
||||||
|
user_badge = BadgeGranter.grant(badge, user)
|
||||||
|
user_badge.granted_by_id.should eq(Discourse.system_user.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not allow a regular user to grant badges' do
|
||||||
|
user_badge = BadgeGranter.grant(badge, user, granted_by: Fabricate(:user))
|
||||||
|
user_badge.should_not be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'increments grant_count on the badge' do
|
||||||
|
BadgeGranter.grant(badge, user)
|
||||||
|
badge.reload.grant_count.should eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'revoke' do
|
||||||
|
|
||||||
|
let!(:user_badge) { BadgeGranter.grant(badge, user) }
|
||||||
|
|
||||||
|
it 'revokes the badge and decrements grant_count' do
|
||||||
|
badge.reload.grant_count.should eq(1)
|
||||||
|
BadgeGranter.revoke(user_badge)
|
||||||
|
UserBadge.where(user: user, badge: badge).first.should_not be_present
|
||||||
|
badge.reload.grant_count.should eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
|
@ -0,0 +1,81 @@
|
||||||
|
module("Discourse.AdminBadgesController");
|
||||||
|
|
||||||
|
test("showDisplayName", function() {
|
||||||
|
var badge, controller;
|
||||||
|
|
||||||
|
badge = Discourse.Badge.create({name: "Test Badge"});
|
||||||
|
controller = testController(Discourse.AdminBadgesController, [badge]);
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
ok(!controller.get('showDisplayName'), "does not show displayName when it is the same as the name");
|
||||||
|
|
||||||
|
this.stub(I18n, "t").returns("translated string");
|
||||||
|
badge = Discourse.Badge.create({name: "Test Badge"});
|
||||||
|
controller = testController(Discourse.AdminBadgesController, [badge]);
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
ok(controller.get('showDisplayName'), "shows the displayName when it is different from the name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("canEditDescription", function() {
|
||||||
|
var badge, controller;
|
||||||
|
|
||||||
|
badge = Discourse.Badge.create({name: "Test Badge"});
|
||||||
|
controller = testController(Discourse.AdminBadgesController, [badge]);
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
ok(controller.get('canEditDescription'), "allows editing description when a translation exists for the badge name");
|
||||||
|
|
||||||
|
this.stub(I18n, "t").returns("translated string");
|
||||||
|
badge = Discourse.Badge.create({name: "Test Badge"});
|
||||||
|
controller = testController(Discourse.AdminBadgesController, [badge]);
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
ok(!controller.get('canEditDescription'), "shows the displayName when it is different from the name");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("newBadge", function() {
|
||||||
|
var controller = testController(Discourse.AdminBadgesController, []);
|
||||||
|
controller.send('newBadge');
|
||||||
|
equal(controller.get('model.length'), 1, "adds a new badge to the list of badges");
|
||||||
|
equal(controller.get('model')[0], controller.get('selectedItem'), "the new badge is selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectBadge", function() {
|
||||||
|
var badge = Discourse.Badge.create({name: "Test Badge"}),
|
||||||
|
controller = testController(Discourse.AdminBadgesController, [badge]);
|
||||||
|
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
equal(controller.get('selectedItem'), badge, "the badge is selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save", function() {
|
||||||
|
var badge = Discourse.Badge.create({name: "Test Badge"}),
|
||||||
|
otherBadge = Discourse.Badge.create({name: "Other Badge"}),
|
||||||
|
controller = testController(Discourse.AdminBadgesController, [badge, otherBadge]);
|
||||||
|
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
this.stub(badge, "save").returns(Ember.RSVP.resolve({}));
|
||||||
|
controller.send("save");
|
||||||
|
ok(badge.save.calledOnce, "called save on the badge");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("destroy", function() {
|
||||||
|
var badge = Discourse.Badge.create({name: "Test Badge"}),
|
||||||
|
otherBadge = Discourse.Badge.create({name: "Other Badge"}),
|
||||||
|
controller = testController(Discourse.AdminBadgesController, [badge, otherBadge]);
|
||||||
|
|
||||||
|
this.stub(badge, 'destroy').returns(Ember.RSVP.resolve({}));
|
||||||
|
|
||||||
|
bootbox.confirm = function(text, yes, no, func) {
|
||||||
|
func(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
controller.send('destroy');
|
||||||
|
ok(!badge.destroy.calledOnce, "badge is not destroyed if they user clicks no");
|
||||||
|
|
||||||
|
bootbox.confirm = function(text, yes, no, func) {
|
||||||
|
func(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
controller.send('selectBadge', badge);
|
||||||
|
controller.send('destroy');
|
||||||
|
ok(badge.destroy.calledOnce, "badge is destroyed if they user clicks yes");
|
||||||
|
});
|
|
@ -0,0 +1,69 @@
|
||||||
|
module("Discourse.Badge");
|
||||||
|
|
||||||
|
test('newBadge', function() {
|
||||||
|
var badge1 = Discourse.Badge.create({name: "New Badge"}),
|
||||||
|
badge2 = Discourse.Badge.create({id: 1, name: "Old Badge"});
|
||||||
|
ok(badge1.get('newBadge'), "badges without ids are new");
|
||||||
|
ok(!badge2.get('newBadge'), "badges with ids are not new");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('displayName', function() {
|
||||||
|
var badge1 = Discourse.Badge.create({id: 1, name: "Test Badge 1"});
|
||||||
|
equal(badge1.get('displayName'), "Test Badge 1", "falls back to the original name in the absence of a translation");
|
||||||
|
|
||||||
|
this.stub(I18n, "t").returnsArg(0);
|
||||||
|
var badge2 = Discourse.Badge.create({id: 2, name: "Test Badge 2"});
|
||||||
|
equal(badge2.get('displayName'), "badges.test_badge_2.name", "uses translation when available");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('translatedDescription', function() {
|
||||||
|
var badge1 = Discourse.Badge.create({id: 1, name: "Test Badge 1"});
|
||||||
|
equal(badge1.get('translatedDescription'), null, "returns null when no translation exists");
|
||||||
|
|
||||||
|
var badge2 = Discourse.Badge.create({id: 2, name: "Test Badge 2"});
|
||||||
|
this.stub(I18n, "t").returns("description translation");
|
||||||
|
equal(badge2.get('translatedDescription'), "description translation", "users translated description");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createFromJson array', function() {
|
||||||
|
var badgesJson = {"badge_types":[{"id":6,"name":"Silver 1","color_hexcode":"#c0c0c0"}],"badges":[{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}]};
|
||||||
|
|
||||||
|
var badges = Discourse.Badge.createFromJson(badgesJson);
|
||||||
|
|
||||||
|
ok(Array.isArray(badges), "returns an array");
|
||||||
|
equal(badges[0].get('name'), "Badge 1", "badge details are set");
|
||||||
|
equal(badges[0].get('badge_type.name'), "Silver 1", "badge_type reference is set");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createFromJson single', function() {
|
||||||
|
var badgeJson = {"badge_types":[{"id":6,"name":"Silver 1","color_hexcode":"#c0c0c0"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}};
|
||||||
|
|
||||||
|
var badge = Discourse.Badge.createFromJson(badgeJson);
|
||||||
|
|
||||||
|
ok(!Array.isArray(badge), "does not returns an array");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updateFromJson', function() {
|
||||||
|
var badgeJson = {"badge_types":[{"id":6,"name":"Silver 1","color_hexcode":"#c0c0c0"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}};
|
||||||
|
var badge = Discourse.Badge.create({name: "Badge 1"});
|
||||||
|
badge.updateFromJson(badgeJson);
|
||||||
|
equal(badge.get('id'), 1126, "id is set");
|
||||||
|
equal(badge.get('badge_type.name'), "Silver 1", "badge_type reference is set");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save', function() {
|
||||||
|
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({}));
|
||||||
|
var badge = Discourse.Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1});
|
||||||
|
badge.save();
|
||||||
|
ok(Discourse.ajax.calledOnce, "saved badge");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy', function() {
|
||||||
|
this.stub(Discourse, 'ajax');
|
||||||
|
var badge = Discourse.Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1});
|
||||||
|
badge.destroy();
|
||||||
|
ok(!Discourse.ajax.calledOnce, "no AJAX call for a new badge");
|
||||||
|
badge.set('id', 3);
|
||||||
|
badge.destroy();
|
||||||
|
ok(Discourse.ajax.calledOnce, "AJAX call was made");
|
||||||
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
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 userBadge = Discourse.UserBadge.createFromJson(json);
|
||||||
|
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");
|
||||||
|
equal(userBadge.get('granted_by.username'), "anne3", "granted_by reference is set");
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
ok(Array.isArray(userBadges), "returns an array");
|
||||||
|
equal(userBadges[0].get('granted_by'), null, "granted_by reference is not set when null");
|
||||||
|
});
|
Loading…
Reference in New Issue