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}}
-
+
{{/if}}
-
-
-
-
{{i18n 'admin.badges.granted_badges'}}
-
-
-
+
{{i18n 'admin.badges.badge'}} |
{{i18n 'admin.badges.granted_by'}} |
+ {{i18n 'admin.badges.reason'}} |
{{i18n 'admin.badges.granted_at'}} |
|
- {{#each}}
+ {{#each userBadge in groupedBadges}}
- {{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}}
|
{{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