Merge pull request #2372 from vikhyat/badge-system
Multiple grant badges
This commit is contained in:
commit
fe594f5d1e
|
@ -9,14 +9,54 @@
|
|||
Discourse.AdminBadgesController = Ember.ArrayController.extend({
|
||||
itemController: 'adminBadge',
|
||||
queryParams: ['badgeId'],
|
||||
badgeId: Em.computed.alias('selectedId'),
|
||||
|
||||
/**
|
||||
ID of the currently selected badge.
|
||||
|
||||
@property badgeId
|
||||
@property selectedId
|
||||
@type {Integer}
|
||||
**/
|
||||
badgeId: Em.computed.alias('selectedItem.id'),
|
||||
selectedId: null,
|
||||
|
||||
/**
|
||||
Badge that is currently selected.
|
||||
|
||||
@property selectedItem
|
||||
@type {Discourse.Badge}
|
||||
**/
|
||||
selectedItem: function() {
|
||||
if (this.get('selectedId') === undefined || this.get('selectedId') === "undefined") {
|
||||
// New Badge
|
||||
return this.get('newBadge');
|
||||
} else {
|
||||
// Existing Badge
|
||||
var selectedId = parseInt(this.get('selectedId'));
|
||||
return this.get('model').filter(function(badge) {
|
||||
return parseInt(badge.get('id')) === selectedId;
|
||||
})[0];
|
||||
}
|
||||
}.property('selectedId', 'newBadge'),
|
||||
|
||||
/**
|
||||
Unsaved badge, if one exists.
|
||||
|
||||
@property newBadge
|
||||
@type {Discourse.Badge}
|
||||
**/
|
||||
newBadge: function() {
|
||||
return this.get('model').filter(function(badge) {
|
||||
return badge.get('id') === undefined;
|
||||
})[0];
|
||||
}.property('model.@each.id'),
|
||||
|
||||
/**
|
||||
Whether a new unsaved badge exists.
|
||||
|
||||
@property newBadgeExists
|
||||
@type {Discourse.Badge}
|
||||
**/
|
||||
newBadgeExists: Em.computed.notEmpty('newBadge'),
|
||||
|
||||
/**
|
||||
We don't allow setting a description if a translation for the given badge
|
||||
|
@ -42,7 +82,7 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({
|
|||
|
||||
@method newBadge
|
||||
**/
|
||||
newBadge: function() {
|
||||
createNewBadge: function() {
|
||||
var badge = Discourse.Badge.create({
|
||||
name: I18n.t('admin.badges.new_badge')
|
||||
});
|
||||
|
@ -57,7 +97,7 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({
|
|||
@param {Discourse.Badge} badge The badge to be selected
|
||||
**/
|
||||
selectBadge: function(badge) {
|
||||
this.set('selectedItem', badge);
|
||||
this.set('selectedId', badge.get('id'));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -80,7 +120,7 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({
|
|||
// Delete immediately if the selected badge is new.
|
||||
if (!this.get('selectedItem.id')) {
|
||||
this.get('model').removeObject(this.get('selectedItem'));
|
||||
this.set('selectedItem', null);
|
||||
this.set('selectedId', null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -90,7 +130,7 @@ Discourse.AdminBadgesController = Ember.ArrayController.extend({
|
|||
var selected = self.get('selectedItem');
|
||||
selected.destroy().then(function() {
|
||||
// Success.
|
||||
self.set('selectedItem', null);
|
||||
self.set('selectedId', null);
|
||||
self.get('model').removeObject(selected);
|
||||
}, function() {
|
||||
// Failure.
|
||||
|
|
|
@ -27,7 +27,7 @@ Discourse.AdminUserBadgesController = Ember.ArrayController.extend({
|
|||
|
||||
var badges = [];
|
||||
this.get('badges').forEach(function(badge) {
|
||||
if (!granted[badge.get('id')]) {
|
||||
if (badge.get('multiple_grant') || !granted[badge.get('id')]) {
|
||||
badges.push(badge);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<button {{action newBadge}} class='btn'><i class="fa fa-plus"></i>{{i18n admin.badges.new}}</button>
|
||||
<button {{action createNewBadge}} {{bind-attr disabled=newBadgeExists}} class='btn'><i class="fa fa-plus"></i>{{i18n admin.badges.new}}</button>
|
||||
</div>
|
||||
|
||||
{{#if selectedItem}}
|
||||
|
@ -47,7 +47,7 @@
|
|||
{{#if controller.canEditDescription}}
|
||||
{{textarea name="description" value=description}}
|
||||
{{else}}
|
||||
{{textarea name="description" value=translatedDescription disabled=true}}
|
||||
{{textarea name="description" value=displayDescription disabled=true}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
|
@ -58,6 +58,13 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span>
|
||||
{{input type="checkbox" checked=multiple_grant disabled=readOnly}}
|
||||
{{i18n admin.badges.multiple_grant}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{#unless readOnly}}
|
||||
<div class='buttons'>
|
||||
<button {{action save}} {{bind-attr disabled=controller.disableSave}} class='btn btn-primary'>{{i18n admin.badges.save}}</button>
|
||||
|
|
|
@ -3,5 +3,9 @@ Discourse.UserBadgeComponent = Ember.Component.extend({
|
|||
|
||||
badgeTypeClassName: function() {
|
||||
return "badge-type-" + this.get('badge.badge_type.name').toLowerCase();
|
||||
}.property('badge.badge_type.name')
|
||||
}.property('badge.badge_type.name'),
|
||||
|
||||
showGrantCount: function() {
|
||||
return this.get('count') && this.get('count') > 1;
|
||||
}.property('count')
|
||||
});
|
||||
|
|
|
@ -40,8 +40,8 @@ Discourse.Badge = Discourse.Model.extend({
|
|||
}.property('name', 'i18nNameKey'),
|
||||
|
||||
/**
|
||||
The i18n translated description for this badge. Returns the original
|
||||
description if no translation exists.
|
||||
The i18n translated description for this badge. Returns the null if no
|
||||
translation exists.
|
||||
|
||||
@property translatedDescription
|
||||
@type {String}
|
||||
|
@ -50,11 +50,23 @@ Discourse.Badge = Discourse.Model.extend({
|
|||
var i18nKey = "badges.badge." + this.get('i18nNameKey') + ".description",
|
||||
translation = I18n.t(i18nKey);
|
||||
if (translation.indexOf(i18nKey) !== -1) {
|
||||
translation = this.get('description');
|
||||
translation = null;
|
||||
}
|
||||
return translation;
|
||||
}.property('i18nNameKey'),
|
||||
|
||||
/**
|
||||
Display-friendly description string. Returns either a translation or the
|
||||
original description string.
|
||||
|
||||
@property displayDescription
|
||||
@type {String}
|
||||
**/
|
||||
displayDescription: function() {
|
||||
var translated = this.get('translatedDescription');
|
||||
return translated === null ? this.get('description') : translated;
|
||||
}.property('description', 'translatedDescription'),
|
||||
|
||||
/**
|
||||
Update this badge with the response returned by the server on save.
|
||||
|
||||
|
@ -103,7 +115,8 @@ Discourse.Badge = Discourse.Model.extend({
|
|||
name: this.get('name'),
|
||||
description: this.get('description'),
|
||||
badge_type_id: this.get('badge_type_id'),
|
||||
allow_title: this.get('allow_title')
|
||||
allow_title: !!this.get('allow_title'),
|
||||
multiple_grant: !!this.get('multiple_grant')
|
||||
}
|
||||
}).then(function(json) {
|
||||
self.updateFromJson(json);
|
||||
|
|
|
@ -79,10 +79,15 @@ Discourse.UserBadge.reopenClass({
|
|||
|
||||
@method findByUsername
|
||||
@param {String} username
|
||||
@param {Object} options
|
||||
@returns {Promise} a promise that resolves to an array of `Discourse.UserBadge`.
|
||||
**/
|
||||
findByUsername: function(username) {
|
||||
return Discourse.ajax("/user_badges.json?username=" + username).then(function(json) {
|
||||
findByUsername: function(username, options) {
|
||||
var url = "/user_badges.json?username=" + username;
|
||||
if (options && options.aggregated) {
|
||||
url += "&aggregated=true";
|
||||
}
|
||||
return Discourse.ajax(url).then(function(json) {
|
||||
return Discourse.UserBadge.createFromJson(json);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
**/
|
||||
Discourse.UserBadgesRoute = Discourse.Route.extend({
|
||||
model: function() {
|
||||
return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username_lower'));
|
||||
return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username_lower'), {aggregated: true});
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{{#each}}
|
||||
<tr>
|
||||
<td class='badge'>{{user-badge badge=this}}</td>
|
||||
<td class='description'>{{translatedDescription}}</td>
|
||||
<td class='description'>{{displayDescription}}</td>
|
||||
<td class='grant-count'>{{i18n badges.granted count=grant_count}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<table class='badges-listing'>
|
||||
<tr>
|
||||
<td class='badge'>{{user-badge badge=this}}</td>
|
||||
<td class='description'>{{translatedDescription}}</td>
|
||||
<td class='description'>{{displayDescription}}</td>
|
||||
<td class='grant-count'>{{i18n badges.granted count=grant_count}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
{{#link-to 'badges.show' badge}}
|
||||
<span {{bind-attr class=":user-badge badgeTypeClassName" data-badge-name="badge.name" title="badge.translatedDescription"}}>
|
||||
<span {{bind-attr class=":user-badge badgeTypeClassName" data-badge-name="badge.name" title="badge.displayDescription"}}>
|
||||
<i class='fa fa-certificate'></i>
|
||||
{{badge.displayName}}
|
||||
{{#if showGrantCount}}
|
||||
<span class="count">(× {{count}})</span>
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/link-to}}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<section class='user-content user-badges-list'>
|
||||
{{#each}}
|
||||
{{user-badge badge=badge}}
|
||||
{{user-badge badge=badge count=count}}
|
||||
{{/each}}
|
||||
</section>
|
||||
|
|
|
@ -44,6 +44,12 @@
|
|||
font-size: 50px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.count {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: scale-color($primary, $lightness: 50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,11 +30,12 @@ class Admin::BadgesController < Admin::AdminController
|
|||
end
|
||||
|
||||
def update_badge_from_params(badge)
|
||||
params.permit(:name, :description, :badge_type_id, :allow_title)
|
||||
params.permit(:name, :description, :badge_type_id, :allow_title, :multiple_grant)
|
||||
badge.name = params[:name]
|
||||
badge.description = params[:description]
|
||||
badge.badge_type = BadgeType.find(params[:badge_type_id])
|
||||
badge.allow_title = params[:allow_title]
|
||||
badge.multiple_grant = params[:multiple_grant]
|
||||
badge
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,6 +16,10 @@ class UserBadgesController < ApplicationController
|
|||
|
||||
user_badges = user_badges.includes(:user, :granted_by, badge: :badge_type)
|
||||
|
||||
if params[:aggregated]
|
||||
user_badges = user_badges.group(:badge_id).select(UserBadge.attribute_names.map {|x| "MAX(#{x}) as #{x}" }, 'COUNT(*) as count')
|
||||
end
|
||||
|
||||
render_serialized(user_badges, UserBadgeSerializer, root: "user_badges")
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ class Badge < ActiveRecord::Base
|
|||
validates :name, presence: true, uniqueness: true
|
||||
validates :badge_type, presence: true
|
||||
validates :allow_title, inclusion: [true, false]
|
||||
validates :multiple_grant, inclusion: [true, false]
|
||||
|
||||
def self.trust_level_badge_ids
|
||||
(1..4).to_a
|
||||
|
@ -15,22 +16,28 @@ class Badge < ActiveRecord::Base
|
|||
save!
|
||||
end
|
||||
|
||||
def single_grant?
|
||||
!self.multiple_grant?
|
||||
end
|
||||
|
||||
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
|
||||
# allow_title :boolean default(FALSE), not null
|
||||
# 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
|
||||
# allow_title :boolean default(FALSE), not null
|
||||
# multiple_grant :boolean default(FALSE), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_badges_on_name (name) UNIQUE
|
||||
# index_badges_on_badge_type_id (badge_type_id)
|
||||
# index_badges_on_name (name) UNIQUE
|
||||
#
|
||||
|
|
|
@ -3,7 +3,7 @@ class UserBadge < ActiveRecord::Base
|
|||
belongs_to :user
|
||||
belongs_to :granted_by, class_name: 'User'
|
||||
|
||||
validates :badge_id, presence: true, uniqueness: {scope: :user_id}
|
||||
validates :badge_id, presence: true, uniqueness: {scope: :user_id}, if: 'badge.single_grant?'
|
||||
validates :user_id, presence: true
|
||||
validates :granted_at, presence: true
|
||||
validates :granted_by, presence: true
|
||||
|
@ -21,5 +21,6 @@ end
|
|||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_user_badges_on_badge_id_and_user_id (badge_id,user_id) UNIQUE
|
||||
# index_user_badges_on_badge_id_and_user_id (badge_id,user_id)
|
||||
# index_user_badges_on_user_id (user_id)
|
||||
#
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class BadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :name, :description, :grant_count, :allow_title
|
||||
attributes :id, :name, :description, :grant_count, :allow_title, :multiple_grant
|
||||
|
||||
has_one :badge_type
|
||||
end
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
class UserBadgeSerializer < ApplicationSerializer
|
||||
attributes :id, :granted_at
|
||||
attributes :id, :granted_at, :count
|
||||
|
||||
has_one :badge
|
||||
has_one :user, serializer: BasicUserSerializer, root: :users
|
||||
has_one :granted_by, serializer: BasicUserSerializer, root: :users
|
||||
|
||||
def include_count?
|
||||
object.respond_to? :count
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,7 +14,7 @@ class BadgeGranter
|
|||
|
||||
user_badge = UserBadge.find_by(badge_id: @badge.id, user_id: @user.id)
|
||||
|
||||
unless user_badge
|
||||
if user_badge.nil? || @badge.multiple_grant?
|
||||
UserBadge.transaction do
|
||||
user_badge = UserBadge.create!(badge: @badge, user: @user,
|
||||
granted_by: @granted_by, granted_at: Time.now)
|
||||
|
|
|
@ -1819,6 +1819,7 @@ en:
|
|||
no_user_badges: "%{name} has not been granted any badges."
|
||||
no_badges: There are no badges that can be granted.
|
||||
allow_title: Allow badge to be used as a title
|
||||
multiple_grant: Can be granted multiple times
|
||||
|
||||
lightbox:
|
||||
download: "download"
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
class AddMultipleAwardToBadges < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :badges, :multiple_grant, :boolean, default: false, null: false
|
||||
|
||||
reversible do |dir|
|
||||
dir.up do
|
||||
remove_index :user_badges, column: [:badge_id, :user_id]
|
||||
add_index :user_badges, [:badge_id, :user_id]
|
||||
end
|
||||
|
||||
dir.down do
|
||||
remove_index :user_badges, column: [:badge_id, :user_id]
|
||||
add_index :user_badges, [:badge_id, :user_id], unique: true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -35,12 +35,12 @@ describe Admin::BadgesController do
|
|||
|
||||
context '.update' do
|
||||
it 'returns success' do
|
||||
xhr :put, :update, id: badge.id, name: "123456", badge_type_id: badge.badge_type_id, allow_title: false
|
||||
xhr :put, :update, id: badge.id, name: "123456", badge_type_id: badge.badge_type_id, allow_title: false, multiple_grant: false
|
||||
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, allow_title: false
|
||||
xhr :put, :update, id: badge.id, name: "123456", badge_type_id: badge.badge_type_id, allow_title: false, multiple_grant: true
|
||||
badge.reload.name.should eq('123456')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,6 +26,14 @@ describe UserBadgesController do
|
|||
parsed = JSON.parse(response.body)
|
||||
parsed["user_badges"].length.should == 1
|
||||
end
|
||||
|
||||
it 'includes counts when passed the aggregate argument' do
|
||||
xhr :get, :index, username: user.username, aggregated: true
|
||||
|
||||
response.status.should == 200
|
||||
parsed = JSON.parse(response.body)
|
||||
parsed["user_badges"].first.has_key?('count').should be_true
|
||||
end
|
||||
end
|
||||
|
||||
context 'create' do
|
||||
|
|
|
@ -15,11 +15,10 @@ test("canEditDescription", function() {
|
|||
ok(!controller.get('canEditDescription'), "shows the displayName when it is different from the name");
|
||||
});
|
||||
|
||||
test("newBadge", function() {
|
||||
test("createNewBadge", function() {
|
||||
var controller = testController(Discourse.AdminBadgesController, []);
|
||||
controller.send('newBadge');
|
||||
controller.send('createNewBadge');
|
||||
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() {
|
||||
|
|
|
@ -18,13 +18,22 @@ test('displayName', function() {
|
|||
|
||||
test('translatedDescription', function() {
|
||||
var badge1 = Discourse.Badge.create({id: 1, name: "Test Badge 1", description: "TEST"});
|
||||
equal(badge1.get('translatedDescription'), "TEST", "returns original description when no translation exists");
|
||||
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('displayDescription', function() {
|
||||
var badge1 = Discourse.Badge.create({id: 1, name: "Test Badge 1", description: "TEST"});
|
||||
equal(badge1.get('displayDescription'), "TEST", "returns original description 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('displayDescription'), "description translation", "users translated description");
|
||||
});
|
||||
|
||||
test('createFromJson array', function() {
|
||||
var badgesJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badges":[{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}]};
|
||||
|
||||
|
|
Loading…
Reference in New Issue