diff --git a/app/assets/javascripts/admin/controllers/admin_badge_controller.js b/app/assets/javascripts/admin/controllers/admin_badge_controller.js index cdc06cfe74c..3fb21eab684 100644 --- a/app/assets/javascripts/admin/controllers/admin_badge_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_badge_controller.js @@ -8,6 +8,8 @@ @module Discourse **/ +var RESERVED_BADGE_COUNT = 100; + Discourse.AdminBadgeController = Discourse.ObjectController.extend({ /** Whether this badge has been selected. @@ -31,5 +33,5 @@ Discourse.AdminBadgeController = Discourse.ObjectController.extend({ @property readOnly @type {Boolean} **/ - readOnly: Ember.computed.lt('model.id', 100) + readOnly: Ember.computed.lt('model.id', RESERVED_BADGE_COUNT) }); diff --git a/app/assets/javascripts/admin/templates/badges.js.handlebars b/app/assets/javascripts/admin/templates/badges.js.handlebars index ae0524deff2..f9c08422ae4 100644 --- a/app/assets/javascripts/admin/templates/badges.js.handlebars +++ b/app/assets/javascripts/admin/templates/badges.js.handlebars @@ -23,7 +23,7 @@
- {{input type="text" name="name" value=name disabled=readOnly}} + {{input type="text" name="name" value=name disabled=readonly}}
{{#if showDisplayName}} @@ -33,6 +33,11 @@ {{/if}} +
+ + {{input type="text" name="name" value=icon disabled=readonly}} +
+
{{view Ember.Select name="badge_type_id" value=badge_type_id diff --git a/app/assets/javascripts/discourse/components/user-badge.js.es6 b/app/assets/javascripts/discourse/components/user-badge.js.es6 index b4e97ecb0d1..0c616cc12fe 100644 --- a/app/assets/javascripts/discourse/components/user-badge.js.es6 +++ b/app/assets/javascripts/discourse/components/user-badge.js.es6 @@ -7,5 +7,7 @@ export default Ember.Component.extend({ showGrantCount: function() { return this.get('count') && this.get('count') > 1; - }.property('count') + }.property('count'), + + isIcon: Em.computed.match('badge.icon', /^fa-/) }); diff --git a/app/assets/javascripts/discourse/models/badge.js b/app/assets/javascripts/discourse/models/badge.js index f709ef29c1a..682534d3875 100644 --- a/app/assets/javascripts/discourse/models/badge.js +++ b/app/assets/javascripts/discourse/models/badge.js @@ -116,7 +116,8 @@ Discourse.Badge = Discourse.Model.extend({ description: this.get('description'), badge_type_id: this.get('badge_type_id'), allow_title: !!this.get('allow_title'), - multiple_grant: !!this.get('multiple_grant') + multiple_grant: !!this.get('multiple_grant'), + icon: this.get('icon') } }).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 a7005341c0e..410dbd4c880 100644 --- a/app/assets/javascripts/discourse/models/user_badge.js +++ b/app/assets/javascripts/discourse/models/user_badge.js @@ -84,8 +84,8 @@ Discourse.UserBadge.reopenClass({ **/ findByUsername: function(username, options) { var url = "/user_badges.json?username=" + username; - if (options && options.aggregated) { - url += "&aggregated=true"; + if (options && options.grouped) { + url += "&grouped=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 c5ce95d1c6a..b6717117fa1 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'), {aggregated: true}); + return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username_lower'), {grouped: 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 201e75aa248..5c8a4eb80ef 100644 --- a/app/assets/javascripts/discourse/templates/badges/index.js.handlebars +++ b/app/assets/javascripts/discourse/templates/badges/index.js.handlebars @@ -2,12 +2,14 @@

{{i18n badges.title}}

- {{#each}} - - - - - - {{/each}} + + {{#each}} + + + + + + {{/each}} +
{{user-badge badge=this}}{{displayDescription}}{{i18n badges.granted count=grant_count}}
{{user-badge badge=this}}{{displayDescription}}{{i18n badges.granted 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 index e113ccf27d7..d97eaeb7ea2 100644 --- a/app/assets/javascripts/discourse/templates/badges/show.js.handlebars +++ b/app/assets/javascripts/discourse/templates/badges/show.js.handlebars @@ -6,11 +6,13 @@ - - - - - + + + + + + +
{{user-badge badge=this}}{{displayDescription}}{{i18n badges.granted count=grant_count}}
{{user-badge badge=this}}{{displayDescription}}{{i18n badges.granted count=grant_count}}
{{#if userBadges}} 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 1e749080a87..e62a8f762d0 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,10 @@ {{#link-to 'badges.show' badge}} - + {{#if isIcon}} + + {{else}} + + {{/if}} {{badge.displayName}} {{#if showGrantCount}} (× {{count}}) diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 8c0cfd42c67..573db4b15d0 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -16,6 +16,11 @@ vertical-align: bottom; } + img { + height: 16px; + width: 16px; + } + &.badge-type-gold .fa { color: #ffd700; } @@ -46,6 +51,12 @@ margin-bottom: 5px; } + img { + display: inline-block; + width: 55px; + height: 55px; + } + .count { display: block; font-size: 0.8em; diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb index 2715f4cdf49..33d7f7645fe 100644 --- a/app/controllers/admin/badges_controller.rb +++ b/app/controllers/admin/badges_controller.rb @@ -36,6 +36,7 @@ class Admin::BadgesController < Admin::AdminController badge.badge_type = BadgeType.find(params[:badge_type_id]) badge.allow_title = params[:allow_title] badge.multiple_grant = params[:multiple_grant] + badge.icon = params[:icon] badge end end diff --git a/app/controllers/badges_controller.rb b/app/controllers/badges_controller.rb index 03f825c5f42..06cd59080d0 100644 --- a/app/controllers/badges_controller.rb +++ b/app/controllers/badges_controller.rb @@ -16,6 +16,14 @@ class BadgesController < ApplicationController def show params.require(:id) badge = Badge.find(params[:id]) + + if current_user + user_badge = UserBadge.find_by(user_id: current_user.id, badge_id: badge.id) + if user_badge && user_badge.notification + user_badge.notification.update_attributes read: true + end + end + serialized = MultiJson.dump(serialize_data(badge, BadgeSerializer, root: "badge")) respond_to do |format| format.html do diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb index 373234bb5ba..e2f96d6ec58 100644 --- a/app/controllers/user_badges_controller.rb +++ b/app/controllers/user_badges_controller.rb @@ -16,7 +16,7 @@ class UserBadgesController < ApplicationController user_badges = user_badges.includes(:user, :granted_by, badge: :badge_type) - if params[:aggregated] + if params[:grouped] user_badges = user_badges.group(:badge_id).select(UserBadge.attribute_names.map {|x| "MAX(#{x}) as #{x}" }, 'COUNT(*) as count') end diff --git a/app/jobs/regular/update_badges.rb b/app/jobs/regular/update_badges.rb new file mode 100644 index 00000000000..3432b02e676 --- /dev/null +++ b/app/jobs/regular/update_badges.rb @@ -0,0 +1,42 @@ +module Jobs + class UpdateBadges < Jobs::Base + + def execute(args) + self.send(args[:action], args) + end + + def trust_level_change(args) + user = User.find(args[:user_id]) + trust_level = user.trust_level + Badge.trust_level_badge_ids.each do |badge_id| + user_badge = UserBadge.find_by(user_id: user.id, badge_id: badge_id) + if user_badge + # Revoke the badge if trust level was lowered. + BadgeGranter.revoke(user_badge) if trust_level < badge_id + else + # Grant the badge if trust level was increased. + badge = Badge.find(badge_id) + BadgeGranter.grant(badge, user) if trust_level >= badge_id + end + end + end + + def post_like(args) + post = Post.find(args[:post_id]) + user = post.user + + # Grant "Welcome" badge to the user if they do not already have it. + BadgeGranter.grant(Badge.find(5), user) + + [{id: 6, count: 10}, {id: 7, count: 25}, {id: 8, count: 100}].each do |b| + if post.like_count >= b[:count] + BadgeGranter.grant(Badge.find(b[:id]), user, post_id: post.id) + else + user_badge = UserBadge.find_by(badge_id: b[:id], user_id: user.id, post_id: post.id) + user_badge && BadgeGranter.revoke(user_badge) + end + end + end + + end +end diff --git a/app/models/badge.rb b/app/models/badge.rb index b891ef02d6f..d090c61a25f 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -35,6 +35,7 @@ end # updated_at :datetime # allow_title :boolean default(FALSE), not null # multiple_grant :boolean default(FALSE), not null +# icon :string(255) default("fa-certificate") # # Indexes # diff --git a/app/models/post_action.rb b/app/models/post_action.rb index 7aab8f6ea9e..86dcf78d814 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -131,13 +131,19 @@ class PostAction < ActiveRecord::Base post.topic.posts_count != 1 end - create( post_id: post.id, - user_id: user.id, - post_action_type_id: post_action_type_id, - message: opts[:message], - staff_took_action: opts[:take_action] || false, - related_post_id: related_post_id, - targets_topic: !!targets_topic ) + post_action = create( post_id: post.id, + user_id: user.id, + post_action_type_id: post_action_type_id, + message: opts[:message], + staff_took_action: opts[:take_action] || false, + related_post_id: related_post_id, + targets_topic: !!targets_topic ) + + if post_action && post_action.is_like? + BadgeGranter.update_badges(action: :post_like, post_id: post.id) + end + + post_action rescue ActiveRecord::RecordNotUnique # can happen despite being .create diff --git a/app/models/user_badge.rb b/app/models/user_badge.rb index e938a855858..24532195171 100644 --- a/app/models/user_badge.rb +++ b/app/models/user_badge.rb @@ -2,22 +2,33 @@ class UserBadge < ActiveRecord::Base belongs_to :badge belongs_to :user belongs_to :granted_by, class_name: 'User' + belongs_to :notification, dependent: :destroy 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 + + after_create do + Badge.increment_counter 'grant_count', self.badge_id + end + + after_destroy do + Badge.decrement_counter 'grant_count', self.badge_id + end end # == Schema Information # # Table name: user_badges # -# id :integer not null, primary key -# badge_id :integer not null -# user_id :integer not null -# granted_at :datetime not null -# granted_by_id :integer not null +# id :integer not null, primary key +# badge_id :integer not null +# user_id :integer not null +# granted_at :datetime not null +# granted_by_id :integer not null +# post_id :integer +# notification_id :integer # # Indexes # diff --git a/app/serializers/badge_serializer.rb b/app/serializers/badge_serializer.rb index ac6e2fcc092..22111deee61 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, :multiple_grant + attributes :id, :name, :description, :grant_count, :allow_title, :multiple_grant, :icon has_one :badge_type end diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb index a15e7d23f62..b132c7af184 100644 --- a/app/services/badge_granter.rb +++ b/app/services/badge_granter.rb @@ -3,6 +3,7 @@ class BadgeGranter def initialize(badge, user, opts={}) @badge, @user, @opts = badge, user, opts @granted_by = opts[:granted_by] || Discourse.system_user + @post_id = opts[:post_id] end def self.grant(badge, user, opts={}) @@ -12,22 +13,22 @@ class BadgeGranter def grant return if @granted_by and !Guardian.new(@granted_by).can_grant_badges?(@user) - user_badge = UserBadge.find_by(badge_id: @badge.id, user_id: @user.id) + user_badge = UserBadge.find_by(badge_id: @badge.id, user_id: @user.id, post_id: @post_id) 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) + granted_by: @granted_by, + granted_at: Time.now, + post_id: @post_id) - Badge.increment_counter 'grant_count', @badge.id if @granted_by != Discourse.system_user StaffActionLogger.new(@granted_by).log_badge_grant(user_badge) end if SiteSetting.enable_badges? - @user.notifications.create(notification_type: Notification.types[:granted_badge], - data: { badge_id: @badge.id, - badge_name: @badge.name }.to_json) + notification = @user.notifications.create(notification_type: Notification.types[:granted_badge], data: { badge_id: @badge.id, badge_name: @badge.name }.to_json) + user_badge.update_attributes notification_id: notification.id end end end @@ -38,7 +39,6 @@ class BadgeGranter def self.revoke(user_badge, options={}) UserBadge.transaction do user_badge.destroy! - Badge.decrement_counter 'grant_count', user_badge.badge_id if options[:revoked_by] StaffActionLogger.new(options[:revoked_by]).log_badge_revoke(user_badge) end @@ -48,30 +48,11 @@ class BadgeGranter user_badge.user.title = nil user_badge.user.save! end - - # Delete notification -- 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 - def self.update_badges(user, opts={}) - if opts.has_key?(:trust_level) - # Update trust level badges. - trust_level = opts[:trust_level] - Badge.trust_level_badge_ids.each do |badge_id| - user_badge = UserBadge.find_by(user_id: user.id, badge_id: badge_id) - if user_badge - # Revoke the badge if trust level was lowered. - BadgeGranter.revoke(user_badge) if trust_level < badge_id - else - # Grant the badge if trust level was increased. - badge = Badge.find(badge_id) - BadgeGranter.grant(badge, user) if trust_level >= badge_id - end - end - end + def self.update_badges(args) + Jobs.enqueue(:update_badges, args) end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3cac7d8d031..a4abbb047db 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1856,6 +1856,7 @@ en: 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 + icon: Icon lightbox: download: "download" @@ -1926,3 +1927,15 @@ en: elder: name: Elder description: Granted global edit, pin, close, archive, split and merge. + welcome: + name: Welcome + description: Received a like. + nice_post: + name: Nice Post + description: Received 10 likes on a post. + good_post: + name: Good Post + description: Received 25 likes on a post. + great_post: + name: Great Post + description: Received 100 likes on a post. diff --git a/db/fixtures/601_badges.rb b/db/fixtures/601_badges.rb index 4e16c48a2d4..e4479ffc860 100644 --- a/db/fixtures/601_badges.rb +++ b/db/fixtures/601_badges.rb @@ -1,3 +1,4 @@ +# Trust level system badges. trust_level_badges = [ {id: 1, name: "Basic User", type: 3}, {id: 2, name: "Regular User", type: 3}, @@ -44,3 +45,29 @@ SQL Badge.where(id: Badge.trust_level_badge_ids).each {|badge| badge.reset_grant_count! } end +# +# Like system badges. +like_badges = [ + {id: 5, name: "Welcome", type: 3, multiple: false}, + {id: 6, name: "Nice Post", type: 3, multiple: true}, + {id: 7, name: "Good Post", type: 2, multiple: true}, + {id: 8, name: "Great Post", type: 1, multiple: true} +] + +like_badges.each do |spec| + Badge.seed do |b| + b.id = spec[:id] + b.name = spec[:name] + b.badge_type_id = spec[:type] + b.multiple_grant = spec[:multiple] + end +end + +# Create an example badge if one does not already exist. +if Badge.find_by(id: 101).nil? + Badge.seed do |b| + b.id = 101 + b.name = "Example Badge" + b.badge_type_id = 3 + end +end diff --git a/db/migrate/20140610012414_add_post_id_to_user_badges.rb b/db/migrate/20140610012414_add_post_id_to_user_badges.rb new file mode 100644 index 00000000000..26ecd890c71 --- /dev/null +++ b/db/migrate/20140610012414_add_post_id_to_user_badges.rb @@ -0,0 +1,5 @@ +class AddPostIdToUserBadges < ActiveRecord::Migration + def change + add_column :user_badges, :post_id, :integer + end +end diff --git a/db/migrate/20140610012833_add_icon_to_badges.rb b/db/migrate/20140610012833_add_icon_to_badges.rb new file mode 100644 index 00000000000..7610385079d --- /dev/null +++ b/db/migrate/20140610012833_add_icon_to_badges.rb @@ -0,0 +1,5 @@ +class AddIconToBadges < ActiveRecord::Migration + def change + add_column :badges, :icon, :string, default: "fa-certificate" + end +end diff --git a/db/migrate/20140617053829_add_notification_id_to_user_badge.rb b/db/migrate/20140617053829_add_notification_id_to_user_badge.rb new file mode 100644 index 00000000000..e066ac3cf02 --- /dev/null +++ b/db/migrate/20140617053829_add_notification_id_to_user_badge.rb @@ -0,0 +1,5 @@ +class AddNotificationIdToUserBadge < ActiveRecord::Migration + def change + add_column :user_badges, :notification_id, :integer + end +end diff --git a/lib/promotion.rb b/lib/promotion.rb index c4ae8f96359..94fa3913d66 100644 --- a/lib/promotion.rb +++ b/lib/promotion.rb @@ -60,7 +60,7 @@ class Promotion @user.user_profile.recook_bio @user.user_profile.save! Group.user_trust_level_change!(@user.id, @user.trust_level) - BadgeGranter.update_badges(@user, trust_level: @user.trust_level) + BadgeGranter.update_badges(action: :trust_level_change, user_id: @user.id) end true diff --git a/spec/controllers/badges_controller_spec.rb b/spec/controllers/badges_controller_spec.rb index 61b6994c716..f45ffd50786 100644 --- a/spec/controllers/badges_controller_spec.rb +++ b/spec/controllers/badges_controller_spec.rb @@ -2,6 +2,11 @@ require 'spec_helper' describe BadgesController do let!(:badge) { Fabricate(:badge) } + let(:user) { Fabricate(:user) } + + before do + SiteSetting.enable_badges = true + end context 'index' do it 'should return a list of all badges' do @@ -20,5 +25,13 @@ describe BadgesController do parsed = JSON.parse(response.body) parsed["badge"].should be_present end + + it "should mark the notification as viewed" do + log_in_user(user) + user_badge = BadgeGranter.grant(badge, user) + user_badge.notification.read.should == false + get :show, id: badge.id, format: :json + user_badge.notification.reload.read.should == true + end end end diff --git a/spec/controllers/user_badges_controller_spec.rb b/spec/controllers/user_badges_controller_spec.rb index a26b52650e6..d93e990ea84 100644 --- a/spec/controllers/user_badges_controller_spec.rb +++ b/spec/controllers/user_badges_controller_spec.rb @@ -28,7 +28,7 @@ describe UserBadgesController do end it 'includes counts when passed the aggregate argument' do - xhr :get, :index, username: user.username, aggregated: true + xhr :get, :index, username: user.username, grouped: true response.status.should == 200 parsed = JSON.parse(response.body) diff --git a/spec/services/badge_granter_spec.rb b/spec/services/badge_granter_spec.rb index 8c8efaf1b3c..f893bd42cf6 100644 --- a/spec/services/badge_granter_spec.rb +++ b/spec/services/badge_granter_spec.rb @@ -72,6 +72,7 @@ describe BadgeGranter do context "update_badges" do let(:user) { Fabricate(:user) } + let(:liker) { Fabricate(:user) } it "grants and revokes trust level badges" do user.change_trust_level!(:elder) @@ -80,6 +81,29 @@ describe BadgeGranter do UserBadge.where(user_id: user.id, badge_id: 1).first.should_not be_nil UserBadge.where(user_id: user.id, badge_id: 2).first.should be_nil end + + it "grants system like badges" do + post = create_post(user: user) + # Welcome badge + PostAction.act(liker, post, PostActionType.types[:like]) + UserBadge.find_by(user_id: user.id, badge_id: 5).should_not be_nil + # Nice post badge + post.update_attributes like_count: 10 + BadgeGranter.update_badges(action: :post_like, post_id: post.id) + UserBadge.find_by(user_id: user.id, badge_id: 6).should_not be_nil + # Good post badge + post.update_attributes like_count: 25 + BadgeGranter.update_badges(action: :post_like, post_id: post.id) + UserBadge.find_by(user_id: user.id, badge_id: 7).should_not be_nil + # Great post badge + post.update_attributes like_count: 100 + BadgeGranter.update_badges(action: :post_like, post_id: post.id) + UserBadge.find_by(user_id: user.id, badge_id: 8).should_not be_nil + # Revoke badges on unlike + post.update_attributes like_count: 99 + BadgeGranter.update_badges(action: :post_like, post_id: post.id) + UserBadge.find_by(user_id: user.id, badge_id: 8).should be_nil + end end end