diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 index 5bb53c4b8..5e6bb325f 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 @@ -13,6 +13,42 @@ export default Ember.ArrayController.extend({ sortProperties: ['granted_at'], sortAscending: false, + groupedBadges: function(){ + const badges = this.get('model'); + + var grouped = _.groupBy(badges, badge => badge.badge_id); + + var expanded = []; + const expandedBadges = badges.get('expandedBadges'); + + _(grouped).each(function(badges){ + var lastGranted = badges[0].granted_at; + + _.each(badges, function(badge) { + lastGranted = lastGranted < badge.granted_at ? badge.granted_at : lastGranted; + }); + + if(badges.length===1 || _.include(expandedBadges, badges[0].badge.id)){ + _.each(badges, badge => expanded.push(badge)); + return; + } + + var result = { + badge: badges[0].badge, + granted_at: lastGranted, + badges: badges, + count: badges.length, + grouped: true + }; + + expanded.push(result); + }); + + return _(expanded).sortBy(group => group.granted_at).reverse().value(); + + + }.property('model', 'model.@each', 'model.expandedBadges.@each'), + /** Array of badges that have not been granted to this user. @@ -45,6 +81,12 @@ export default Ember.ArrayController.extend({ actions: { + expandGroup: function(userBadge){ + const model = this.get('model'); + model.set('expandedBadges', model.get('expandedBadges') || []); + model.get('expandedBadges').pushObject(userBadge.badge.id); + }, + /** Grant the selected badge to the user. @@ -53,7 +95,8 @@ export default Ember.ArrayController.extend({ **/ grantBadge: function(badgeId) { var self = this; - Discourse.UserBadge.grant(badgeId, this.get('user.username')).then(function(userBadge) { + Discourse.UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(function(userBadge) { + self.set('badgeReason', ''); self.pushObject(userBadge); Ember.run.next(function() { // Update the selected badge ID after the combobox has re-rendered. diff --git a/app/assets/javascripts/admin/templates/user_badges.hbs b/app/assets/javascripts/admin/templates/user_badges.hbs index a4bfc236e..5d55a2d0b 100644 --- a/app/assets/javascripts/admin/templates/user_badges.hbs +++ b/app/assets/javascripts/admin/templates/user_badges.hbs @@ -9,40 +9,53 @@ {{#loading-spinner condition=loading}}

{{i18n 'admin.badges.grant_badge'}}

+
{{#if noBadges}}

{{i18n 'admin.badges.no_badges'}}

{{else}} -
+
+
+ {{combo-box valueAttribute="id" value=controller.selectedBadgeId content=controller.grantableBadges}} +
+ +
{{/if}} -
-
- -

{{i18n 'admin.badges.granted_badges'}}

-
- - +
+ - {{#each}} + {{#each userBadge in groupedBadges}} - + - + + {{else}} diff --git a/app/assets/javascripts/discourse/models/user_badge.js b/app/assets/javascripts/discourse/models/user_badge.js index 9297f5e52..158e70e46 100644 --- a/app/assets/javascripts/discourse/models/user_badge.js +++ b/app/assets/javascripts/discourse/models/user_badge.js @@ -7,6 +7,11 @@ @module Discourse **/ Discourse.UserBadge = Discourse.Model.extend({ + postUrl: function() { + if(this.get('topic_title')) { + return "/t/-/" + this.get('topic_id') + "/" + this.get('post_number'); + } + }.property(), // avoid the extra bindings for now /** Revoke this badge. @@ -93,7 +98,7 @@ Discourse.UserBadge.reopenClass({ @returns {Promise} a promise that resolves to an array of `Discourse.UserBadge`. **/ findByUsername: function(username, options) { - var url = "/users/" + username + "/badges_json.json"; + var url = "/user-badges/" + username + ".json"; if (options && options.grouped) { url += "?grouped=true"; } @@ -128,12 +133,13 @@ Discourse.UserBadge.reopenClass({ @param {String} username username of the user to be granted the badge. @returns {Promise} a promise that resolves to an instance of `Discourse.UserBadge`. **/ - grant: function(badgeId, username) { + grant: function(badgeId, username, reason) { return Discourse.ajax("/user_badges", { type: "POST", data: { username: username, - badge_id: badgeId + badge_id: badgeId, + reason: reason } }).then(function(json) { return Discourse.UserBadge.createFromJson(json); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 9ff7cf1f6..eb9b02fd3 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1440,3 +1440,9 @@ tr.not-activated { .preview { margin-top: 5px; } + +table#user-badges { + .reason { + max-width: 200px; + } +} diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb index 7ee6555ab..b1cf39845 100644 --- a/app/controllers/user_badges_controller.rb +++ b/app/controllers/user_badges_controller.rb @@ -25,8 +25,10 @@ class UserBadgesController < ApplicationController end user_badges = user_badges.includes(badge: [:badge_grouping, :badge_type]) + .includes(post: :topic) + .includes(:granted_by) - render_serialized(user_badges, BasicUserBadgeSerializer, root: "user_badges") + render_serialized(user_badges, DetailedUserBadgeSerializer, root: "user_badges") end def create @@ -39,9 +41,22 @@ class UserBadgesController < ApplicationController end badge = fetch_badge_from_params - user_badge = BadgeGranter.grant(badge, user, granted_by: current_user) + post_id = nil - render_serialized(user_badge, UserBadgeSerializer, root: "user_badge") + if params[:reason].present? + path = URI.parse(params[:reason]).path rescue nil + route = Rails.application.routes.recognize_path(path) if path + if route + topic_id = route[:topic_id].to_i + post_number = route[:post_number] || 1 + + post_id = Post.find_by(topic_id: topic_id, post_number: post_number).try(:id) if topic_id > 0 + end + end + + user_badge = BadgeGranter.grant(badge, user, granted_by: current_user, post_id: post_id) + + render_serialized(user_badge, DetailedUserBadgeSerializer, root: "user_badge") end def destroy diff --git a/app/serializers/detailed_user_badge_serializer.rb b/app/serializers/detailed_user_badge_serializer.rb new file mode 100644 index 000000000..1fa408844 --- /dev/null +++ b/app/serializers/detailed_user_badge_serializer.rb @@ -0,0 +1,26 @@ +class DetailedUserBadgeSerializer < BasicUserBadgeSerializer + has_one :granted_by + + attributes :post_number, :topic_id, :topic_title + + def include_post_number? + object.post + end + + alias :include_topic_id? :include_post_number? + alias :include_topic_title? :include_post_number? + + + def post_number + object.post.post_number if object.post + end + + def topic_id + object.post.topic_id if object.post + end + + def topic_title + object.post.topic.title if object.post && object.post.topic + end + +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d0c31b483..41c824d70 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2194,10 +2194,13 @@ en: modal_title: Badge Groupings granted_by: Granted By granted_at: Granted At + reason_help: (A link to a post or topic) save: Save delete: Delete delete_confirm: Are you sure you want to delete this badge? revoke: Revoke + reason: Reason + expand: Expand … revoke_confirm: Are you sure you want to revoke this badge? edit_badges: Edit Badges grant_badge: Grant Badge diff --git a/config/routes.rb b/config/routes.rb index 94d7d9ebd..fe45618ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -261,7 +261,7 @@ Discourse::Application.routes.draw do get "users/by-external/:external_id" => "users#show" get "users/:username/flagged-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/deleted-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "users/:username/badges_json" => "user_badges#username" + get "user-badges/:username" => "user_badges#username" post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar" get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/ } diff --git a/spec/controllers/user_badges_controller_spec.rb b/spec/controllers/user_badges_controller_spec.rb index 867751b56..1bfb3ad31 100644 --- a/spec/controllers/user_badges_controller_spec.rb +++ b/spec/controllers/user_badges_controller_spec.rb @@ -63,13 +63,23 @@ describe UserBadgesController do it 'grants badges from staff' do admin = Fabricate(:admin) + post = create_post + log_in_user admin + StaffActionLogger.any_instance.expects(:log_badge_grant).once - xhr :post, :create, badge_id: badge.id, username: user.username + + xhr :post, :create, badge_id: badge.id, + username: user.username, + reason: Discourse.base_url + post.url + expect(response.status).to eq(200) + user_badge = UserBadge.find_by(user: user, badge: badge) + expect(user_badge).to be_present expect(user_badge.granted_by).to eq(admin) + expect(user_badge.post_id).to eq(post.id) end it 'does not grant badges from regular api calls' do
{{i18n 'admin.badges.badge'}} {{i18n 'admin.badges.granted_by'}}{{i18n 'admin.badges.reason'}} {{i18n 'admin.badges.granted_at'}}
{{user-badge badge=badge}}{{user-badge badge=userBadge.badge count=userBadge.count}} - {{#link-to 'adminUser' badge.granted_by}} - {{avatar granted_by imageSize="tiny"}} - {{granted_by.username}} + {{#link-to 'adminUser' userBadge.badge.granted_by}} + {{avatar userBadge.granted_by imageSize="tiny"}} + {{userBadge.granted_by.username}} {{/link-to}} {{age-with-tooltip granted_at}} + {{#if userBadge.postUrl}} + {{userBadge.topic_title}} + {{/if}} + {{age-with-tooltip userBadge.granted_at}} - + {{#if userBadge.grouped}} + + {{else}} + + {{/if}}