diff --git a/app/assets/javascripts/admin/templates/user_index.js.handlebars b/app/assets/javascripts/admin/templates/user_index.js.handlebars index bdbdde76194..89ce976275d 100644 --- a/app/assets/javascripts/admin/templates/user_index.js.handlebars +++ b/app/assets/javascripts/admin/templates/user_index.js.handlebars @@ -81,7 +81,7 @@
{{i18n admin.badges.title}}
- TODO featured badges + {{i18n badges.badge_count count=badge_count}}
{{#link-to 'adminUser.badges' this class="btn"}}{{i18n admin.badges.edit_badges}}{{/link-to}} diff --git a/app/assets/javascripts/discourse/components/user_badge_component.js b/app/assets/javascripts/discourse/components/user_badge_component.js new file mode 100644 index 00000000000..daf3e6d305a --- /dev/null +++ b/app/assets/javascripts/discourse/components/user_badge_component.js @@ -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') +}); diff --git a/app/assets/javascripts/discourse/controllers/notification_controller.js b/app/assets/javascripts/discourse/controllers/notification_controller.js index 19db113cad7..765ca1d85a5 100644 --- a/app/assets/javascripts/discourse/controllers/notification_controller.js +++ b/app/assets/javascripts/discourse/controllers/notification_controller.js @@ -8,6 +8,9 @@ Discourse.NotificationController = Discourse.ObjectController.extend({ }.property(), link: function() { + if (this.get('data.badge_id')) { + return '' + this.get('data.badge_name') + ''; + } if (this.blank("data.topic_title")) { return ""; } diff --git a/app/assets/javascripts/discourse/controllers/user_controller.js b/app/assets/javascripts/discourse/controllers/user_controller.js index 65b03627449..1054198763b 100644 --- a/app/assets/javascripts/discourse/controllers/user_controller.js +++ b/app/assets/javascripts/discourse/controllers/user_controller.js @@ -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); diff --git a/app/assets/javascripts/discourse/models/badge.js b/app/assets/javascripts/discourse/models/badge.js index 3a4ad3b8753..4bc61a95eca 100644 --- a/app/assets/javascripts/discourse/models/badge.js +++ b/app/assets/javascripts/discourse/models/badge.js @@ -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); + }); } }); diff --git a/app/assets/javascripts/discourse/models/user_badge.js b/app/assets/javascripts/discourse/models/user_badge.js index 3d521c1fa12..b19a3a25a7c 100644 --- a/app/assets/javascripts/discourse/models/user_badge.js +++ b/app/assets/javascripts/discourse/models/user_badge.js @@ -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`. diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js index 0dcc467aeb6..fb739f58587 100644 --- a/app/assets/javascripts/discourse/routes/application_routes.js +++ b/app/assets/javascripts/discourse/routes/application_routes.js @@ -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'}); + }); }); diff --git a/app/assets/javascripts/discourse/routes/badges_index_route.js b/app/assets/javascripts/discourse/routes/badges_index_route.js new file mode 100644 index 00000000000..6563af0d777 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/badges_index_route.js @@ -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(); + } +}); diff --git a/app/assets/javascripts/discourse/routes/badges_show_route.js b/app/assets/javascripts/discourse/routes/badges_show_route.js new file mode 100644 index 00000000000..d0769325040 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/badges_show_route.js @@ -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); + } +}); diff --git a/app/assets/javascripts/discourse/routes/user_badges_route.js b/app/assets/javascripts/discourse/routes/user_badges_route.js new file mode 100644 index 00000000000..97ad6ddf92f --- /dev/null +++ b/app/assets/javascripts/discourse/routes/user_badges_route.js @@ -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'}); + } +}); diff --git a/app/assets/javascripts/discourse/templates/badges/index.js.handlebars b/app/assets/javascripts/discourse/templates/badges/index.js.handlebars new file mode 100644 index 00000000000..334ce7c59d9 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/badges/index.js.handlebars @@ -0,0 +1,13 @@ +
+

{{i18n badges.title}}

+ + + {{#each}} + + + + + + {{/each}} +
{{user-badge badge=this}}{{description}}{{i18n badges.awarded count=grant_count}}
+
diff --git a/app/assets/javascripts/discourse/templates/badges/show.js.handlebars b/app/assets/javascripts/discourse/templates/badges/show.js.handlebars new file mode 100644 index 00000000000..ce161dcf02b --- /dev/null +++ b/app/assets/javascripts/discourse/templates/badges/show.js.handlebars @@ -0,0 +1,27 @@ +
+

+ {{#link-to 'badges.index'}}{{i18n badges.title}}{{/link-to}} + + {{name}} +

+ + + + + + + +
{{user-badge badge=this}}{{description}}{{i18n badges.awarded count=grant_count}}
+ + {{#if userBadges}} +

{{i18n users}}

+
+ {{#each userBadges}} + {{#link-to 'userActivity' user}} + {{avatar user imageSize="large"}} + {{/link-to}} + {{/each}} + {{else}} +
{{i18n loading}}
+ {{/if}} +
diff --git a/app/assets/javascripts/discourse/templates/components/user-badge.js.handlebars b/app/assets/javascripts/discourse/templates/components/user-badge.js.handlebars new file mode 100644 index 00000000000..ca2070b7e3d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/user-badge.js.handlebars @@ -0,0 +1,6 @@ +{{#link-to 'badges.show' badge}} + + + {{badge.name}} + +{{/link-to}} diff --git a/app/assets/javascripts/discourse/templates/poster_expansion.handlebars b/app/assets/javascripts/discourse/templates/poster_expansion.handlebars index 1e9f3e99ed7..874935fda54 100644 --- a/app/assets/javascripts/discourse/templates/poster_expansion.handlebars +++ b/app/assets/javascripts/discourse/templates/poster_expansion.handlebars @@ -10,7 +10,7 @@ {{#if showBadges}}
{{#each user.featured_user_badges}} - {{badge.name}} + {{user-badge badge=badge}} {{/each}} {{#if showMoreBadges}} {{i18n badges.more_badges count=moreBadgesCount}} diff --git a/app/assets/javascripts/discourse/templates/user/badges.js.handlebars b/app/assets/javascripts/discourse/templates/user/badges.js.handlebars new file mode 100644 index 00000000000..6cbf2003cf9 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/user/badges.js.handlebars @@ -0,0 +1,5 @@ +
+ {{#each}} + {{user-badge badge=badge}} + {{/each}} +
diff --git a/app/assets/javascripts/discourse/templates/user/user.js.handlebars b/app/assets/javascripts/discourse/templates/user/user.js.handlebars index 882f9999c08..0cf25536aad 100644 --- a/app/assets/javascripts/discourse/templates/user/user.js.handlebars +++ b/app/assets/javascripts/discourse/templates/user/user.js.handlebars @@ -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'}} + + {{i18n badges.title}} + ({{badge_count}}) + + {{/link-to}} + {{/link-to}} + {{/if}} {{#if canSeePrivateMessages}} diff --git a/app/assets/stylesheets/desktop/poster_expansion.scss b/app/assets/stylesheets/desktop/poster_expansion.scss index 8a90a8694c4..d3a7a566c69 100644 --- a/app/assets/stylesheets/desktop/poster_expansion.scss +++ b/app/assets/stylesheets/desktop/poster_expansion.scss @@ -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; -} diff --git a/app/assets/stylesheets/desktop/user-badges.scss b/app/assets/stylesheets/desktop/user-badges.scss new file mode 100644 index 00000000000..2be02acb1fb --- /dev/null +++ b/app/assets/stylesheets/desktop/user-badges.scss @@ -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; + } +} diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb index 1cfb1ef8edb..92b119af80c 100644 --- a/app/controllers/admin/badges_controller.rb +++ b/app/controllers/admin/badges_controller.rb @@ -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") diff --git a/app/controllers/badges_controller.rb b/app/controllers/badges_controller.rb new file mode 100644 index 00000000000..a8908afd4f8 --- /dev/null +++ b/app/controllers/badges_controller.rb @@ -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 diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb index ef5bbf3345f..daba1ddc9f6 100644 --- a/app/controllers/user_badges_controller.rb +++ b/app/controllers/user_badges_controller.rb @@ -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 diff --git a/app/models/badge_type.rb b/app/models/badge_type.rb index ede44018534..abebf9495dc 100644 --- a/app/models/badge_type.rb +++ b/app/models/badge_type.rb @@ -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 # diff --git a/app/models/notification.rb b/app/models/notification.rb index 77b53b854b1..a1a459e7ebd 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -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 diff --git a/app/models/user.rb b/app/models/user.rb index 23f3daa09d3..238a2a0f6c1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb index 685c34c1c45..32ba08cff34 100644 --- a/app/serializers/admin_detailed_user_serializer.rb +++ b/app/serializers/admin_detailed_user_serializer.rb @@ -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 diff --git a/app/serializers/badge_serializer.rb b/app/serializers/badge_serializer.rb index 05173cc30cf..34b72959846 100644 --- a/app/serializers/badge_serializer.rb +++ b/app/serializers/badge_serializer.rb @@ -1,5 +1,5 @@ class BadgeSerializer < ApplicationSerializer - attributes :id, :name, :description + attributes :id, :name, :description, :grant_count has_one :badge_type end diff --git a/app/serializers/badge_type_serializer.rb b/app/serializers/badge_type_serializer.rb index bd6ed272d53..199c99a377a 100644 --- a/app/serializers/badge_type_serializer.rb +++ b/app/serializers/badge_type_serializer.rb @@ -1,3 +1,3 @@ class BadgeTypeSerializer < ApplicationSerializer - attributes :id, :name, :color_hexcode + attributes :id, :name end diff --git a/app/serializers/user_badge_serializer.rb b/app/serializers/user_badge_serializer.rb index d91dfb9162b..eda18b11ea9 100644 --- a/app/serializers/user_badge_serializer.rb +++ b/app/serializers/user_badge_serializer.rb @@ -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 diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 1431ccd9f8b..fc597762a98 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -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 diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb index 5511cf9f50f..53b3d1fb965 100644 --- a/app/services/badge_granter.rb +++ b/app/services/badge_granter.rb @@ -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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0820df06c56..c493fe74b7e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -598,6 +598,7 @@ en: moved_post: " {{username}} moved {{link}}" total_flagged: "total flagged posts" linked: " {{username}} {{link}}" + granted_badge: " {{link}}" upload_selector: title: "Add an image" @@ -1768,12 +1769,16 @@ en: mark_watching: 'm then w 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. diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 37af0e54611..ac2c6bd5611 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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 diff --git a/config/routes.rb b/config/routes.rb index 89fd083787b..bb3b6e64b75 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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. diff --git a/db/fixtures/700_badge_types.rb b/db/fixtures/700_badge_types.rb index 475817e85ea..2cdec2b4169 100644 --- a/db/fixtures/700_badge_types.rb +++ b/db/fixtures/700_badge_types.rb @@ -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 diff --git a/db/migrate/20140416235757_remove_color_hexcode_from_badge_types.rb b/db/migrate/20140416235757_remove_color_hexcode_from_badge_types.rb new file mode 100644 index 00000000000..9a19c17a8f6 --- /dev/null +++ b/db/migrate/20140416235757_remove_color_hexcode_from_badge_types.rb @@ -0,0 +1,5 @@ +class RemoveColorHexcodeFromBadgeTypes < ActiveRecord::Migration + def change + remove_column :badge_types, :color_hexcode, :string + end +end diff --git a/spec/controllers/admin/badges_controller_spec.rb b/spec/controllers/admin/badges_controller_spec.rb index 5359c278f74..62674af346a 100644 --- a/spec/controllers/admin/badges_controller_spec.rb +++ b/spec/controllers/admin/badges_controller_spec.rb @@ -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 diff --git a/spec/controllers/badges_controller_spec.rb b/spec/controllers/badges_controller_spec.rb new file mode 100644 index 00000000000..945d641512b --- /dev/null +++ b/spec/controllers/badges_controller_spec.rb @@ -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 diff --git a/spec/controllers/user_badges_controller_spec.rb b/spec/controllers/user_badges_controller_spec.rb index 9d3fdbf1f43..87fc240e6ee 100644 --- a/spec/controllers/user_badges_controller_spec.rb +++ b/spec/controllers/user_badges_controller_spec.rb @@ -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 diff --git a/spec/fabricators/badge_fabricator.rb b/spec/fabricators/badge_fabricator.rb index 7ba52d8b4b5..d3a5f78d7f7 100644 --- a/spec/fabricators/badge_fabricator.rb +++ b/spec/fabricators/badge_fabricator.rb @@ -1,6 +1,5 @@ Fabricator(:badge_type) do name { sequence(:name) {|i| "Silver #{i}" } } - color_hexcode "c0c0c0" end Fabricator(:badge) do diff --git a/spec/models/badge.rb b/spec/models/badge.rb index 8416fc854f0..ab8ab50c3fd 100644 --- a/spec/models/badge.rb +++ b/spec/models/badge.rb @@ -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) } diff --git a/spec/models/badge_type.rb b/spec/models/badge_type.rb index bc716b857ef..552518e7c79 100644 --- a/spec/models/badge_type.rb +++ b/spec/models/badge_type.rb @@ -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 diff --git a/spec/models/user_badge.rb b/spec/models/user_badge.rb index 383db76a101..7c233051172 100644 --- a/spec/models/user_badge.rb +++ b/spec/models/user_badge.rb @@ -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)) } diff --git a/spec/services/badge_granter_spec.rb b/spec/services/badge_granter_spec.rb index 6801bc969d1..476bc6a4b4a 100644 --- a/spec/services/badge_granter_spec.rb +++ b/spec/services/badge_granter_spec.rb @@ -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 diff --git a/test/javascripts/models/badge_test.js b/test/javascripts/models/badge_test.js index dbd1c7549d2..a4365e352a4 100644 --- a/test/javascripts/models/badge_test.js +++ b/test/javascripts/models/badge_test.js @@ -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"); diff --git a/test/javascripts/models/user_badge_test.js b/test/javascripts/models/user_badge_test.js index eb56100df94..589d34752ed 100644 --- a/test/javascripts/models/user_badge_test.js +++ b/test/javascripts/models/user_badge_test.js @@ -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) {