diff --git a/app/assets/javascripts/admin/controllers/admin_badges_controller.js b/app/assets/javascripts/admin/controllers/admin_badges_controller.js
index 9ec65709a2b..baac6bf7fb7 100644
--- a/app/assets/javascripts/admin/controllers/admin_badges_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_badges_controller.js
@@ -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.
diff --git a/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js b/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js
index 4c1fdea0cbd..1cfc771957c 100644
--- a/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js
+++ b/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js
@@ -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);
}
});
diff --git a/app/assets/javascripts/admin/templates/badges.js.handlebars b/app/assets/javascripts/admin/templates/badges.js.handlebars
index 6b1b513c023..ae0524deff2 100644
--- a/app/assets/javascripts/admin/templates/badges.js.handlebars
+++ b/app/assets/javascripts/admin/templates/badges.js.handlebars
@@ -14,7 +14,7 @@
{{/each}}
-
+
{{#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}}
@@ -58,6 +58,13 @@
+
+
+ {{input type="checkbox" checked=multiple_grant disabled=readOnly}}
+ {{i18n admin.badges.multiple_grant}}
+
+
+
{{#unless readOnly}}
diff --git a/app/assets/javascripts/discourse/components/user_badge_component.js b/app/assets/javascripts/discourse/components/user_badge_component.js
index daf3e6d305a..725169b2366 100644
--- a/app/assets/javascripts/discourse/components/user_badge_component.js
+++ b/app/assets/javascripts/discourse/components/user_badge_component.js
@@ -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')
});
diff --git a/app/assets/javascripts/discourse/models/badge.js b/app/assets/javascripts/discourse/models/badge.js
index f95775b681c..f709ef29c1a 100644
--- a/app/assets/javascripts/discourse/models/badge.js
+++ b/app/assets/javascripts/discourse/models/badge.js
@@ -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);
diff --git a/app/assets/javascripts/discourse/models/user_badge.js b/app/assets/javascripts/discourse/models/user_badge.js
index 23b987e6002..a7005341c0e 100644
--- a/app/assets/javascripts/discourse/models/user_badge.js
+++ b/app/assets/javascripts/discourse/models/user_badge.js
@@ -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);
});
},
diff --git a/app/assets/javascripts/discourse/routes/user_badges_route.js b/app/assets/javascripts/discourse/routes/user_badges_route.js
index 97ad6ddf92f..c5ce95d1c6a 100644
--- a/app/assets/javascripts/discourse/routes/user_badges_route.js
+++ b/app/assets/javascripts/discourse/routes/user_badges_route.js
@@ -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) {
diff --git a/app/assets/javascripts/discourse/templates/badges/index.js.handlebars b/app/assets/javascripts/discourse/templates/badges/index.js.handlebars
index 0551537fea3..201e75aa248 100644
--- a/app/assets/javascripts/discourse/templates/badges/index.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/badges/index.js.handlebars
@@ -5,7 +5,7 @@
{{#each}}
{{user-badge badge=this}} |
- {{translatedDescription}} |
+ {{displayDescription}} |
{{i18n badges.granted count=grant_count}} |
{{/each}}
diff --git a/app/assets/javascripts/discourse/templates/badges/show.js.handlebars b/app/assets/javascripts/discourse/templates/badges/show.js.handlebars
index 26f7a090611..551a655fd42 100644
--- a/app/assets/javascripts/discourse/templates/badges/show.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/badges/show.js.handlebars
@@ -8,7 +8,7 @@
{{user-badge badge=this}} |
- {{translatedDescription}} |
+ {{displayDescription}} |
{{i18n badges.granted count=grant_count}} |
diff --git a/app/assets/javascripts/discourse/templates/components/user-badge.js.handlebars b/app/assets/javascripts/discourse/templates/components/user-badge.js.handlebars
index 64419e5278d..1e749080a87 100644
--- a/app/assets/javascripts/discourse/templates/components/user-badge.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/components/user-badge.js.handlebars
@@ -1,6 +1,9 @@
{{#link-to 'badges.show' badge}}
-
+
{{badge.displayName}}
+ {{#if showGrantCount}}
+ (× {{count}})
+ {{/if}}
{{/link-to}}
diff --git a/app/assets/javascripts/discourse/templates/user/badges.js.handlebars b/app/assets/javascripts/discourse/templates/user/badges.js.handlebars
index 6cbf2003cf9..897e46c3f74 100644
--- a/app/assets/javascripts/discourse/templates/user/badges.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/user/badges.js.handlebars
@@ -1,5 +1,5 @@
{{#each}}
- {{user-badge badge=badge}}
+ {{user-badge badge=badge count=count}}
{{/each}}
diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss
index b6b0e48ec69..5f013f1da59 100644
--- a/app/assets/stylesheets/common/base/user-badges.scss
+++ b/app/assets/stylesheets/common/base/user-badges.scss
@@ -44,6 +44,12 @@
font-size: 50px;
margin-bottom: 5px;
}
+
+ .count {
+ display: block;
+ font-size: 0.8em;
+ color: scale-color($primary, $lightness: 50%);
+ }
}
}
diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb
index 08991e8ef67..2715f4cdf49 100644
--- a/app/controllers/admin/badges_controller.rb
+++ b/app/controllers/admin/badges_controller.rb
@@ -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
diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb
index ad11f75f426..373234bb5ba 100644
--- a/app/controllers/user_badges_controller.rb
+++ b/app/controllers/user_badges_controller.rb
@@ -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
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 42231734656..b891ef02d6f 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -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
#
diff --git a/app/models/user_badge.rb b/app/models/user_badge.rb
index edf24521eb4..e938a855858 100644
--- a/app/models/user_badge.rb
+++ b/app/models/user_badge.rb
@@ -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)
#
diff --git a/app/serializers/badge_serializer.rb b/app/serializers/badge_serializer.rb
index 90be747c4c9..ac6e2fcc092 100644
--- a/app/serializers/badge_serializer.rb
+++ b/app/serializers/badge_serializer.rb
@@ -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
diff --git a/app/serializers/user_badge_serializer.rb b/app/serializers/user_badge_serializer.rb
index f751ecd6dd7..c2235dcf071 100644
--- a/app/serializers/user_badge_serializer.rb
+++ b/app/serializers/user_badge_serializer.rb
@@ -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
diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb
index 7fc2ff14932..a15e7d23f62 100644
--- a/app/services/badge_granter.rb
+++ b/app/services/badge_granter.rb
@@ -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)
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 977a8cc42e7..ac7f0ed9f38 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -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"
diff --git a/db/migrate/20140520062826_add_multiple_award_to_badges.rb b/db/migrate/20140520062826_add_multiple_award_to_badges.rb
new file mode 100644
index 00000000000..4e7fb1bdfa7
--- /dev/null
+++ b/db/migrate/20140520062826_add_multiple_award_to_badges.rb
@@ -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
diff --git a/spec/controllers/admin/badges_controller_spec.rb b/spec/controllers/admin/badges_controller_spec.rb
index b507c6a7a61..478b61b6828 100644
--- a/spec/controllers/admin/badges_controller_spec.rb
+++ b/spec/controllers/admin/badges_controller_spec.rb
@@ -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
diff --git a/spec/controllers/user_badges_controller_spec.rb b/spec/controllers/user_badges_controller_spec.rb
index 5320760fee2..a26b52650e6 100644
--- a/spec/controllers/user_badges_controller_spec.rb
+++ b/spec/controllers/user_badges_controller_spec.rb
@@ -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
diff --git a/test/javascripts/admin/controllers/admin_badges_controller_test.js b/test/javascripts/admin/controllers/admin_badges_controller_test.js
index f310d785278..ccf6aa7cfd6 100644
--- a/test/javascripts/admin/controllers/admin_badges_controller_test.js
+++ b/test/javascripts/admin/controllers/admin_badges_controller_test.js
@@ -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() {
diff --git a/test/javascripts/models/badge_test.js b/test/javascripts/models/badge_test.js
index ff688ee8e92..f04279d974b 100644
--- a/test/javascripts/models/badge_test.js
+++ b/test/javascripts/models/badge_test.js
@@ -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}]};