Merge pull request #2271 from vikhyat/badge-system
Badge system updates
This commit is contained in:
commit
87f37b3ee9
|
@ -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}}
|
||||
|
|
|
@ -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')
|
||||
});
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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`.
|
||||
|
||||
|
|
|
@ -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'});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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'});
|
||||
}
|
||||
});
|
|
@ -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>
|
|
@ -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>
|
|
@ -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}}
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<section class='user-content user-badges-list'>
|
||||
{{#each}}
|
||||
{{user-badge badge=badge}}
|
||||
{{/each}}
|
||||
</section>
|
|
@ -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}}
|
||||
|
|
|
@ -102,39 +102,12 @@
|
|||
font-size: 14px;
|
||||
margin-bottom: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.more-user-badges {
|
||||
@extend .user-badge;
|
||||
padding: 4px 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 {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.badge-type-1 .fa-certificate {
|
||||
color: #A67D3D;
|
||||
}
|
||||
|
||||
.badge-type-2 .fa-certificate {
|
||||
color: silver;
|
||||
}
|
||||
|
||||
.badge-type-1 .fa-certificate {
|
||||
color: gold;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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
|
|
@ -1,8 +1,14 @@
|
|||
class UserBadgesController < ApplicationController
|
||||
def index
|
||||
params.require(:username)
|
||||
user = fetch_user_from_params
|
||||
render_serialized(user.user_badges, UserBadgeSerializer, root: "user_badges")
|
||||
params.permit(:username)
|
||||
if params[:username]
|
||||
user = fetch_user_from_params
|
||||
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
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class BadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :description
|
||||
attributes :id, :name, :description, :grant_count
|
||||
|
||||
has_one :badge_type
|
||||
end
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
class BadgeTypeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :color_hexcode
|
||||
attributes :id, :name
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
class RemoveColorHexcodeFromBadgeTypes < ActiveRecord::Migration
|
||||
def change
|
||||
remove_column :badge_types, :color_hexcode, :string
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
Fabricator(:badge_type) do
|
||||
name { sequence(:name) {|i| "Silver #{i}" } }
|
||||
color_hexcode "c0c0c0"
|
||||
end
|
||||
|
||||
Fabricator(:badge) do
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue