Merge pull request #2271 from vikhyat/badge-system

Badge system updates
This commit is contained in:
Sam 2014-04-17 16:22:41 +10:00
commit 87f37b3ee9
45 changed files with 372 additions and 110 deletions

View File

@ -81,7 +81,7 @@
<div class='display-row'>
<div class='field'>{{i18n admin.badges.title}}</div>
<div class='value'>
TODO featured badges
{{i18n badges.badge_count count=badge_count}}
</div>
<div class='controls'>
{{#link-to 'adminUser.badges' this class="btn"}}{{i18n admin.badges.edit_badges}}{{/link-to}}

View File

@ -0,0 +1,7 @@
Discourse.UserBadgeComponent = Ember.Component.extend({
tagName: 'span',
badgeTypeClassName: function() {
return "badge-type-" + this.get('badge.badge_type.name').toLowerCase();
}.property('badge.badge_type.name')
});

View File

@ -8,6 +8,9 @@ Discourse.NotificationController = Discourse.ObjectController.extend({
}.property(),
link: function() {
if (this.get('data.badge_id')) {
return '<a href="/badges/' + this.get('data.badge_id') + '/' + this.get('data.badge_name').replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase() + '">' + this.get('data.badge_name') + '</a>';
}
if (this.blank("data.topic_title")) {
return "";
}

View File

@ -18,6 +18,10 @@ Discourse.UserController = Discourse.ObjectController.extend({
return this.get('viewingSelf') || Discourse.User.currentProp('admin');
}.property('viewingSelf'),
showBadges: function() {
return Discourse.SiteSettings.enable_badges;
}.property(),
privateMessageView: function() {
return (this.get('userActionType') === Discourse.UserAction.TYPES.messages_sent) ||
(this.get('userActionType') === Discourse.UserAction.TYPES.messages_received);

View File

@ -164,8 +164,21 @@ Discourse.Badge.reopenClass({
@returns {Promise} a promise that resolves to an array of `Discourse.Badge`
**/
findAll: function() {
return Discourse.ajax('/admin/badges').then(function(badgesJson) {
return Discourse.ajax('/badges.json').then(function(badgesJson) {
return Discourse.Badge.createFromJson(badgesJson);
});
},
/**
Returns a `Discourse.Badge` that has the given ID.
@method findById
@param {Number} id ID of the badge
@returns {Promise} a promise that resolves to a `Discourse.Badge`
**/
findById: function(id) {
return Discourse.ajax("/badges/" + id).then(function(badgeJson) {
return Discourse.Badge.createFromJson(badgeJson);
});
}
});

View File

@ -54,6 +54,9 @@ Discourse.UserBadge.reopenClass({
userBadges = userBadges.map(function(userBadgeJson) {
var userBadge = Discourse.UserBadge.create(userBadgeJson);
userBadge.set('badge', badges[userBadge.get('badge_id')]);
if (userBadge.get('user_id')) {
userBadge.set('user', users[userBadge.get('user_id')]);
}
if (userBadge.get('granted_by_id')) {
userBadge.set('granted_by', users[userBadge.get('granted_by_id')]);
}
@ -71,6 +74,7 @@ Discourse.UserBadge.reopenClass({
Find all badges for a given username.
@method findByUsername
@param {String} username
@returns {Promise} a promise that resolves to an array of `Discourse.UserBadge`.
**/
findByUsername: function(username) {
@ -79,6 +83,19 @@ Discourse.UserBadge.reopenClass({
});
},
/**
Find all badge grants for a given badge ID.
@method findById
@param {String} badgeId
@returns {Promise} a promise that resolves to an array of `Discourse.UserBadge`.
**/
findByBadgeId: function(badgeId) {
return Discourse.ajax("/user_badges.json?badge_id=" + badgeId).then(function(json) {
return Discourse.UserBadge.createFromJson(json);
});
},
/**
Grant the badge having id `badgeId` to the user identified by `username`.

View File

@ -78,6 +78,8 @@ Discourse.Route.buildRoutes(function() {
});
});
this.route('badges');
this.resource('userPrivateMessages', { path: '/private-messages' }, function() {
this.route('mine');
this.route('unread');
@ -94,4 +96,8 @@ Discourse.Route.buildRoutes(function() {
this.route('signup', {path: '/signup'});
this.route('login', {path: '/login'});
this.resource('badges', function() {
this.route('show', {path: '/:id/:slug'});
});
});

View File

@ -0,0 +1,13 @@
/**
Shows a list of all badges.
@class BadgesIndexRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.BadgesIndexRoute = Discourse.Route.extend({
model: function() {
return Discourse.Badge.findAll();
}
});

View File

@ -0,0 +1,24 @@
/**
Shows a particular badge.
@class BadgesShowRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.BadgesShowRoute = Ember.Route.extend({
serialize: function(model) {
return {id: model.get('id'), slug: model.get('name').replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase()};
},
model: function(params) {
return Discourse.Badge.findById(params.id);
},
setupController: function(controller, model) {
Discourse.UserBadge.findByBadgeId(model.get('id')).then(function(userBadges) {
controller.set('userBadges', userBadges);
});
controller.set('model', model);
}
});

View File

@ -0,0 +1,25 @@
/**
This route shows a user's badges.
@class UserBadgesRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.UserBadgesRoute = Discourse.Route.extend({
model: function() {
return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username_lower'));
},
setupController: function(controller, model) {
this.controllerFor('user').set('indexStream', false);
if (this.controllerFor('user_activity').get('content')) {
this.controllerFor('user_activity').set('userActionType', -1);
}
controller.set('model', model);
},
renderTemplate: function() {
this.render('user/badges', {into: 'user', outlet: 'userOutlet'});
}
});

View File

@ -0,0 +1,13 @@
<div class='container'>
<h1>{{i18n badges.title}}</h1>
<table class='badges-listing'>
{{#each}}
<tr>
<td class='badge'>{{user-badge badge=this}}</td>
<td class='description'>{{description}}</td>
<td class='grant-count'>{{i18n badges.awarded count=grant_count}}</td>
</tr>
{{/each}}
</table>
</div>

View File

@ -0,0 +1,27 @@
<div class='container'>
<h1>
{{#link-to 'badges.index'}}{{i18n badges.title}}{{/link-to}}
<i class='fa fa-angle-right'></i>
{{name}}
</h1>
<table class='badges-listing'>
<tr>
<td class='badge'>{{user-badge badge=this}}</td>
<td class='description'>{{description}}</td>
<td class='grant-count'>{{i18n badges.awarded count=grant_count}}</td>
</tr>
</table>
{{#if userBadges}}
<h2>{{i18n users}}</h2>
<br>
{{#each userBadges}}
{{#link-to 'userActivity' user}}
{{avatar user imageSize="large"}}
{{/link-to}}
{{/each}}
{{else}}
<div class='spinner'>{{i18n loading}}</div>
{{/if}}
</div>

View File

@ -0,0 +1,6 @@
{{#link-to 'badges.show' badge}}
<span {{bind-attr class=":user-badge badgeTypeClassName" data-badge-name="badge.name" title="badge.description"}}>
<i class='fa fa-certificate'></i>
{{badge.name}}
</span>
{{/link-to}}

View File

@ -10,7 +10,7 @@
{{#if showBadges}}
<div class="badge-section">
{{#each user.featured_user_badges}}
<span class="user-badge badge-type-{{unbound badge.badge_type_id}}"><i class='fa fa-certificate'></i> {{badge.name}}</span>
{{user-badge badge=badge}}
{{/each}}
{{#if showMoreBadges}}
<span class="btn more-user-badges">{{i18n badges.more_badges count=moreBadgesCount}}</span>

View File

@ -0,0 +1,5 @@
<section class='user-content user-badges-list'>
{{#each}}
{{user-badge badge=badge}}
{{/each}}
</section>

View File

@ -13,6 +13,16 @@
{{#each stat in statsExcludingPms}}
{{discourse-activity-filter content=stat user=model userActionType=userActionType indexStream=indexStream}}
{{/each}}
{{#if showBadges}}
{{#link-to 'user.badges' tagName="li"}}
{{#link-to 'user.badges'}}
<i class='glyph fa fa-certificate'></i>
{{i18n badges.title}}
<span class='count'>({{badge_count}})</span>
<span class='fa fa-chevron-right'></span>
{{/link-to}}
{{/link-to}}
{{/if}}
</ul>
{{#if canSeePrivateMessages}}

View File

@ -102,39 +102,12 @@
font-size: 14px;
margin-bottom: -8px;
}
}
}
}
.user-badge, .more-user-badges {
font-size: 12px;
margin: 0;
line-height: 16px;
display: inline-block;
.fa {
padding-right: 5px;
font-size: 16px;
}
}
.user-badge {
padding: 3px 8px;
border: 1px solid $secondary-border-color;
}
.more-user-badges {
@extend .user-badge;
padding: 4px 8px;
}
.badge-type-1 .fa-certificate {
color: #A67D3D;
}
}
.badge-type-2 .fa-certificate {
color: silver;
}
.badge-type-1 .fa-certificate {
color: gold;
}

View File

@ -0,0 +1,75 @@
/* Default badge styles. */
.user-badge {
padding: 3px 8px;
color: $primary_text_color;
border: 1px solid $secondary-border-color;
font-size: $base-font-size * 0.86;
line-height: 16px;
margin: 0;
display: inline-block;
background-color: $primary_background_color;
.fa {
padding-right: 3px;
font-size: 1.4em;
vertical-align: bottom;
}
&.badge-type-gold .fa {
color: #ffd700;
}
&.badge-type-silver .fa {
color: #c0c0c0;
}
&.badge-type-bronze .fa {
color: #cd7f32;
}
}
/* User badge listing. */
.user-badges-list {
text-align: center;
.user-badge {
max-width: 80px;
text-align: center;
vertical-align: top;
margin: 10px;
border: none;
.fa {
display: block;
font-size: 50px;
margin-bottom: 5px;
}
}
}
/* Badge listing in /badges. */
table.badges-listing {
margin: 20px 0;
border-bottom: 1px solid $primary-border-color;
.user-badge {
font-size: $base-font-size;
}
td {
padding: 10px 20px;
}
td.grant-count {
font-size: 0.8em;
color: $secondary_text_color;
}
td.badge, td.grant-count {
white-space: nowrap;
}
tr {
border-top: 1px solid $primary-border-color;
}
}

View File

@ -1,9 +1,4 @@
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")

View File

@ -0,0 +1,12 @@
class BadgesController < ApplicationController
def index
badges = Badge.all.to_a
render_serialized(badges, BadgeSerializer, root: "badges")
end
def show
params.require(:id)
badge = Badge.find(params[:id])
render_serialized(badge, BadgeSerializer, root: "badge")
end
end

View File

@ -1,8 +1,14 @@
class UserBadgesController < ApplicationController
def index
params.require(:username)
params.permit(:username)
if params[:username]
user = fetch_user_from_params
render_serialized(user.user_badges, UserBadgeSerializer, root: "user_badges")
user_badges = user.user_badges
else
badge = fetch_badge_from_params
user_badges = badge.user_badges.order('granted_at DESC').limit(20).to_a
end
render_serialized(user_badges, UserBadgeSerializer, root: "user_badges")
end
def create

View File

@ -2,7 +2,6 @@ class BadgeType < ActiveRecord::Base
has_many :badges
validates :name, presence: true, uniqueness: true
validates :color_hexcode, presence: true
end
# == Schema Information
@ -11,7 +10,6 @@ end
#
# id :integer not null, primary key
# name :string(255) not null
# color_hexcode :string(255) not null
# created_at :datetime
# updated_at :datetime
#

View File

@ -28,7 +28,7 @@ class Notification < ActiveRecord::Base
@types ||= Enum.new(
:mentioned, :replied, :quoted, :edited, :liked, :private_message,
:invited_to_private_message, :invitee_accepted, :posted, :moved_post,
:linked
:linked, :granted_badge
)
end

View File

@ -492,6 +492,14 @@ class User < ActiveRecord::Base
Summarize.new(bio_cooked).summary
end
def badge_count
user_badges.count
end
def featured_user_badges
user_badges.joins(:badge).order('badges.badge_type_id ASC, badges.grant_count ASC').includes(:granted_by, badge: :badge_type).limit(3)
end
def self.count_by_signup_date(sinceDaysAgo=30)
where('created_at > ?', sinceDaysAgo.days.ago).group('date(created_at)').order('date(created_at)').count
end

View File

@ -15,7 +15,8 @@ class AdminDetailedUserSerializer < AdminUserSerializer
:can_delete_all_posts,
:can_be_deleted,
:suspend_reason,
:primary_group_id
:primary_group_id,
:badge_count
has_one :approved_by, serializer: BasicUserSerializer, embed: :objects
has_one :api_key, serializer: ApiKeySerializer, embed: :objects

View File

@ -1,5 +1,5 @@
class BadgeSerializer < ApplicationSerializer
attributes :id, :name, :description
attributes :id, :name, :description, :grant_count
has_one :badge_type
end

View File

@ -1,3 +1,3 @@
class BadgeTypeSerializer < ApplicationSerializer
attributes :id, :name, :color_hexcode
attributes :id, :name
end

View File

@ -1,6 +1,7 @@
class UserBadgeSerializer < ApplicationSerializer
attributes :id, :granted_at
has_one :user
has_one :badge
has_one :granted_by, serializer: BasicUserSerializer, root: :users
end

View File

@ -131,12 +131,4 @@ class UserSerializer < BasicUserSerializer
CategoryUser.lookup(object, :watching).pluck(:category_id)
end
def badge_count
object.user_badges.count
end
def featured_user_badges
# The three rarest badges this user has received should be featured.
object.user_badges.joins(:badge).order('badges.grant_count ASC').includes(:granted_by, badge: :badge_type).limit(3)
end
end

View File

@ -23,6 +23,10 @@ class BadgeGranter
if @granted_by != Discourse.system_user
StaffActionLogger.new(@granted_by).log_badge_grant(user_badge)
end
@user.notifications.create(notification_type: Notification.types[:granted_badge],
data: { badge_id: @badge.id,
badge_name: @badge.name }.to_json)
end
end
@ -32,10 +36,14 @@ class BadgeGranter
def self.revoke(user_badge, options={})
UserBadge.transaction do
user_badge.destroy!
Badge.decrement_counter 'grant_count', user_badge.badge.id
Badge.decrement_counter 'grant_count', user_badge.badge_id
if options[:revoked_by]
StaffActionLogger.new(options[:revoked_by]).log_badge_revoke(user_badge)
end
# Revoke badge -- This is inefficient, but not very easy to optimize unless
# the data hash is converted into a hstore.
notification = user_badge.user.notifications.where(notification_type: Notification.types[:granted_badge]).where("data LIKE ?", "%" + user_badge.badge_id.to_s + "%").select {|n| n.data_hash["badge_id"] == user_badge.badge_id }.first
notification && notification.destroy
end
end

View File

@ -598,6 +598,7 @@ en:
moved_post: "<i title='moved post' class='fa fa-arrow-right'></i> {{username}} moved {{link}}"
total_flagged: "total flagged posts"
linked: "<i title='linked post' class='fa fa-arrow-left'></i> {{username}} {{link}}"
granted_badge: "<i title='badge granted' class='fa fa-certificate'></i> {{link}}"
upload_selector:
title: "Add an image"
@ -1768,12 +1769,16 @@ en:
mark_watching: '<b>m</b> then <b>w</b> Mark topic as watching'
badges:
title: Badges
badge_count:
one: "1 Badge"
other: "%{count} Badges"
more_badges:
one: "+1 More"
other: "+%{count} More"
awarded:
one: "1 awarded"
other: "%{count} awarded"
example_badge:
name: Example Badge
description: This is a generic example badge.

View File

@ -888,6 +888,7 @@ en:
invited_to_private_message: "%{display_username} invited you to a private message: %{link}"
invitee_accepted: "%{display_username} accepted your invitation"
linked: "%{display_username} linked you in %{link}"
granted_badge: "You were granted the badge %{link}"
search:
within_post: "#%{post_number} by %{username}: %{excerpt}"
@ -1474,9 +1475,3 @@ 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

View File

@ -195,6 +195,7 @@ Discourse::Application.routes.draw do
post "users/:username/send_activation_email" => "users#send_activation_email", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/activity/:filter" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/badges" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}
delete "users/:username" => "users#destroy", constraints: {username: USERNAME_ROUTE_FORMAT}
get "uploads/:site/:id/:sha.:extension" => "uploads#show", constraints: {site: /\w+/, id: /\d+/, sha: /[a-z0-9]{15,16}/i, extension: /\w{2,}/}
@ -242,6 +243,8 @@ Discourse::Application.routes.draw do
end
resources :user_actions
resources :badges, only: [:index]
get "/badges/:id(/:slug)" => "badges#show"
resources :user_badges, only: [:index, :create, :destroy]
# We've renamed popular to latest. If people access it we want a permanent redirect.

View File

@ -1,17 +1,14 @@
BadgeType.seed do |b|
b.id = 1
b.name = I18n.t('badges.types.gold')
b.color_hexcode = "ffd700"
b.name = "Gold"
end
BadgeType.seed do |b|
b.id = 2
b.name = I18n.t('badges.types.silver')
b.color_hexcode = "c0c0c0"
b.name = "Silver"
end
BadgeType.seed do |b|
b.id = 3
b.name = I18n.t('badges.types.bronze')
b.color_hexcode = "cd7f32"
b.name = "Bronze"
end

View File

@ -0,0 +1,5 @@
class RemoveColorHexcodeFromBadgeTypes < ActiveRecord::Migration
def change
remove_column :badge_types, :color_hexcode, :string
end
end

View File

@ -9,18 +9,6 @@ describe Admin::BadgesController 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

View File

@ -0,0 +1,24 @@
require 'spec_helper'
describe BadgesController do
let!(:badge) { Fabricate(:badge) }
context 'index' do
it 'should return a list of all badges' do
xhr :get, :index
response.status.should == 200
parsed = JSON.parse(response.body)
parsed["badges"].length.should == 1
end
end
context 'show' do
it "should return a badge" do
xhr :get, :show, id: badge.id
response.status.should == 200
parsed = JSON.parse(response.body)
parsed["badge"].should be_present
end
end
end

View File

@ -5,21 +5,27 @@ describe UserBadgesController do
let(:badge) { Fabricate(:badge) }
context 'index' do
before do
@user_badge = BadgeGranter.grant(badge, user)
end
let!(:user_badge) { UserBadge.create(badge: badge, user: user, granted_by: Discourse.system_user, granted_at: Time.now) }
it 'requires username to be specified' do
it 'requires username or badge_id to be specified' do
expect { xhr :get, :index }.to raise_error
end
it 'returns the user\'s badges' do
it 'returns user_badges for a user' do
xhr :get, :index, username: user.username
response.status.should == 200
parsed = JSON.parse(response.body)
parsed["user_badges"].length.should == 1
end
it 'returns user_badges for a badge' do
xhr :get, :index, badge_id: badge.id
response.status.should == 200
parsed = JSON.parse(response.body)
parsed["user_badges"].length.should == 1
end
end
context 'create' do
@ -62,21 +68,19 @@ describe UserBadgesController do
end
context 'destroy' do
before do
@user_badge = BadgeGranter.grant(badge, user)
end
let!(:user_badge) { UserBadge.create(badge: badge, user: user, granted_by: Discourse.system_user, granted_at: Time.now) }
it 'checks that the user is authorized to revoke a badge' do
xhr :delete, :destroy, id: @user_badge.id
xhr :delete, :destroy, id: user_badge.id
response.status.should == 403
end
it 'revokes the badge' do
log_in :admin
StaffActionLogger.any_instance.expects(:log_badge_revoke).once
xhr :delete, :destroy, id: @user_badge.id
xhr :delete, :destroy, id: user_badge.id
response.status.should == 200
UserBadge.where(id: @user_badge.id).first.should be_nil
UserBadge.where(id: user_badge.id).first.should be_nil
end
end
end

View File

@ -1,6 +1,5 @@
Fabricator(:badge_type) do
name { sequence(:name) {|i| "Silver #{i}" } }
color_hexcode "c0c0c0"
end
Fabricator(:badge) do

View File

@ -3,9 +3,6 @@ require_dependency 'badge'
describe Badge do
it { should belong_to :badge_type }
it { should have_many(:user_badges).dependent(:destroy) }
context 'validations' do
before(:each) { Fabricate(:badge) }

View File

@ -3,10 +3,7 @@ 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

View File

@ -3,10 +3,6 @@ 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)) }

View File

@ -40,9 +40,10 @@ describe BadgeGranter do
user_badge.should_not be_present
end
it 'increments grant_count on the badge' do
it 'increments grant_count on the badge and creates a notification' do
BadgeGranter.grant(badge, user)
badge.reload.grant_count.should eq(1)
user.notifications.where(notification_type: Notification.types[:granted_badge]).first.data_hash["badge_id"].should == badge.id
end
end
@ -52,12 +53,13 @@ describe BadgeGranter do
let(:admin) { Fabricate(:admin) }
let!(:user_badge) { BadgeGranter.grant(badge, user) }
it 'revokes the badge and decrements grant_count' do
it 'revokes the badge, deletes the notification and decrements grant_count' do
badge.reload.grant_count.should eq(1)
StaffActionLogger.any_instance.expects(:log_badge_revoke).with(user_badge)
BadgeGranter.revoke(user_badge, revoked_by: admin)
UserBadge.where(user: user, badge: badge).first.should_not be_present
badge.reload.grant_count.should eq(0)
user.notifications.where(notification_type: Notification.types[:granted_badge]).should be_empty
end
end

View File

@ -26,7 +26,7 @@ test('translatedDescription', function() {
});
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 badgesJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badges":[{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}]};
var badges = Discourse.Badge.createFromJson(badgesJson);
@ -36,7 +36,7 @@ test('createFromJson array', function() {
});
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 badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}};
var badge = Discourse.Badge.createFromJson(badgeJson);
@ -44,7 +44,7 @@ test('createFromJson single', function() {
});
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 badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"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");

View File

@ -1,7 +1,7 @@
module("Discourse.UserBadge");
var singleBadgeJson = {"badges":[{"id":874,"name":"Badge 2","description":null,"badge_type_id":7}],"badge_types":[{"id":7,"name":"Silver 2","color_hexcode":"#c0c0c0"}],"users":[{"id":13470,"username":"anne3","avatar_template":"//www.gravatar.com/avatar/a4151b1fd72089c54e2374565a87da7f.png?s={size}\u0026r=pg\u0026d=identicon"}],"user_badge":{"id":665,"granted_at":"2014-03-09T20:30:01.190-04:00","badge_id":874,"granted_by_id":13470}},
multipleBadgesJson = {"badges":[{"id":880,"name":"Badge 8","description":null,"badge_type_id":13}],"badge_types":[{"id":13,"name":"Silver 8","color_hexcode":"#c0c0c0"}],"users":[],"user_badges":[{"id":668,"granted_at":"2014-03-09T20:30:01.420-04:00","badge_id":880,"granted_by_id":null}]};
var singleBadgeJson = {"badges":[{"id":874,"name":"Badge 2","description":null,"badge_type_id":7}],"badge_types":[{"id":7,"name":"Silver 2"}],"users":[{"id":13470,"username":"anne3","avatar_template":"//www.gravatar.com/avatar/a4151b1fd72089c54e2374565a87da7f.png?s={size}\u0026r=pg\u0026d=identicon"}],"user_badge":{"id":665,"granted_at":"2014-03-09T20:30:01.190-04:00","badge_id":874,"granted_by_id":13470}},
multipleBadgesJson = {"badges":[{"id":880,"name":"Badge 8","description":null,"badge_type_id":13}],"badge_types":[{"id":13,"name":"Silver 8"}],"users":[],"user_badges":[{"id":668,"granted_at":"2014-03-09T20:30:01.420-04:00","badge_id":880,"granted_by_id":null}]};
test('createFromJson single', function() {
var userBadge = Discourse.UserBadge.createFromJson(singleBadgeJson);
@ -25,6 +25,14 @@ test('findByUsername', function() {
ok(Discourse.ajax.calledOnce, "makes an AJAX call");
});
test('findByBadgeId', function() {
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(multipleBadgesJson));
Discourse.UserBadge.findByBadgeId(880).then(function(badges) {
ok(Array.isArray(badges), "returns an array");
});
ok(Discourse.ajax.calledOnce, "makes an AJAX call");
});
test('grant', function() {
this.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(singleBadgeJson));
Discourse.UserBadge.grant(1, "username").then(function(userBadge) {