- 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}}
+
+ {{user-badge badge=this}} |
+ {{description}} |
+ {{i18n badges.awarded 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
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) {