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}]};