mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 09:36:19 -05:00
Merge pull request #2115 from vikhyat/badge-system
Initial badge system implementation
This commit is contained in:
commit
fe63db7953
41 changed files with 1186 additions and 0 deletions
|
@ -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;
|
||||
}.property(),
|
||||
|
||||
showBadges: function() {
|
||||
return Discourse.SiteSettings.enable_badges;
|
||||
}.property(),
|
||||
|
||||
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
|
||||
|
||||
actions: {
|
||||
|
|
14
app/assets/javascripts/admin/routes/admin_badges_route.js
Normal file
14
app/assets/javascripts/admin/routes/admin_badges_route.js
Normal file
|
@ -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.api'}}{{i18n admin.api.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}}
|
||||
</ul>
|
||||
|
||||
|
|
62
app/assets/javascripts/admin/templates/badges.js.handlebars
Normal file
62
app/assets/javascripts/admin/templates/badges.js.handlebars
Normal file
|
@ -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>
|
||||
</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"}}>
|
||||
|
|
164
app/assets/javascripts/discourse/models/badge.js
Normal file
164
app/assets/javascripts/discourse/models/badge.js
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
57
app/assets/javascripts/discourse/models/user_badge.js
Normal file
57
app/assets/javascripts/discourse/models/user_badge.js
Normal file
|
@ -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
|
||||
.customize {
|
||||
.nav.nav-pills {
|
||||
|
|
44
app/controllers/admin/badges_controller.rb
Normal file
44
app/controllers/admin/badges_controller.rb
Normal file
|
@ -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
|
58
app/controllers/user_badges_controller.rb
Normal file
58
app/controllers/user_badges_controller.rb
Normal file
|
@ -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
|
24
app/models/badge.rb
Normal file
24
app/models/badge.rb
Normal file
|
@ -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
|
||||
#
|
21
app/models/badge_type.rb
Normal file
21
app/models/badge_type.rb
Normal file
|
@ -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_actions, dependent: :destroy
|
||||
has_many :post_actions, dependent: :destroy
|
||||
has_many :user_badges, dependent: :destroy
|
||||
has_many :email_logs, dependent: :destroy
|
||||
has_many :post_timings
|
||||
has_many :topic_allowed_users, dependent: :destroy
|
||||
|
|
26
app/models/user_badge.rb
Normal file
26
app/models/user_badge.rb
Normal file
|
@ -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)
|
||||
#
|
5
app/serializers/badge_serializer.rb
Normal file
5
app/serializers/badge_serializer.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class BadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :description
|
||||
|
||||
has_one :badge_type
|
||||
end
|
3
app/serializers/badge_type_serializer.rb
Normal file
3
app/serializers/badge_type_serializer.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class BadgeTypeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :color_hexcode
|
||||
end
|
6
app/serializers/user_badge_serializer.rb
Normal file
6
app/serializers/user_badge_serializer.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class UserBadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :granted_at
|
||||
|
||||
has_one :badge
|
||||
has_one :granted_by, serializer: BasicUserSerializer, root: :users
|
||||
end
|
34
app/services/badge_granter.rb
Normal file
34
app/services/badge_granter.rb
Normal file
|
@ -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
|
|
@ -1667,6 +1667,18 @@ en:
|
|||
uncategorized: 'Uncategorized'
|
||||
backups: "Backups"
|
||||
|
||||
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:
|
||||
download: "download"
|
||||
|
||||
|
@ -1708,3 +1720,8 @@ en:
|
|||
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_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.
|
||||
|
|
|
@ -665,6 +665,8 @@ en:
|
|||
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"
|
||||
|
||||
enable_badges: "Enable the badge system (experimental)"
|
||||
|
||||
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_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."
|
||||
|
@ -1418,3 +1420,9 @@ en:
|
|||
message_to_blank: "message.to is blank"
|
||||
text_part_body_blank: "text_part.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
|
||||
|
||||
resources :badges, constraints: AdminConstraint.new do
|
||||
collection do
|
||||
get "types" => "badges#badge_types"
|
||||
end
|
||||
end
|
||||
|
||||
get "memory_stats"=> "diagnostics#memory_stats", constraints: AdminConstraint.new
|
||||
|
||||
end # admin namespace
|
||||
|
@ -235,6 +241,8 @@ Discourse::Application.routes.draw do
|
|||
end
|
||||
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.
|
||||
get "popular" => "list#popular_redirect"
|
||||
get "popular/more" => "list#popular_redirect"
|
||||
|
|
|
@ -75,6 +75,9 @@ basic:
|
|||
default: 50
|
||||
redirect_new_users_to_top_page_duration:
|
||||
default: 7
|
||||
enable_badges:
|
||||
client: true
|
||||
default: false
|
||||
|
||||
users:
|
||||
enable_sso:
|
||||
|
|
17
db/fixtures/700_badge_types.rb
Normal file
17
db/fixtures/700_badge_types.rb
Normal file
|
@ -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
|
12
db/migrate/20140304200606_create_badge_types.rb
Normal file
12
db/migrate/20140304200606_create_badge_types.rb
Normal file
|
@ -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
|
14
db/migrate/20140304201403_create_badges.rb
Normal file
14
db/migrate/20140304201403_create_badges.rb
Normal file
|
@ -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
|
12
db/migrate/20140305100909_create_user_badges.rb
Normal file
12
db/migrate/20140305100909_create_user_badges.rb
Normal file
|
@ -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_see_flags? :can_moderate?
|
||||
alias :can_send_activation_email? :can_moderate?
|
||||
alias :can_grant_badges? :can_moderate?
|
||||
|
||||
|
||||
|
||||
|
|
60
spec/controllers/admin/badges_controller_spec.rb
Normal file
60
spec/controllers/admin/badges_controller_spec.rb
Normal file
|
@ -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
|
79
spec/controllers/user_badges_controller_spec.rb
Normal file
79
spec/controllers/user_badges_controller_spec.rb
Normal file
|
@ -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
|
9
spec/fabricators/badge_fabricator.rb
Normal file
9
spec/fabricators/badge_fabricator.rb
Normal file
|
@ -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
|
17
spec/models/badge.rb
Normal file
17
spec/models/badge.rb
Normal file
|
@ -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
|
||||
|
12
spec/models/badge_type.rb
Normal file
12
spec/models/badge_type.rb
Normal file
|
@ -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
|
20
spec/models/user_badge.rb
Normal file
20
spec/models/user_badge.rb
Normal file
|
@ -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_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(:post_timings) }
|
||||
it { should have_many(:topic_allowed_users).dependent(:destroy) }
|
||||
|
|
61
spec/services/badge_granter_spec.rb
Normal file
61
spec/services/badge_granter_spec.rb
Normal file
|
@ -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");
|
||||
});
|
69
test/javascripts/models/badge_test.js
Normal file
69
test/javascripts/models/badge_test.js
Normal file
|
@ -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");
|
||||
});
|
19
test/javascripts/models/user_badge_test.js
Normal file
19
test/javascripts/models/user_badge_test.js
Normal file
|
@ -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 a new issue