diff --git a/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js b/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js index af120daca11..4c1fdea0cbd 100644 --- a/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js +++ b/app/assets/javascripts/admin/controllers/admin_user_badges_controller.js @@ -57,7 +57,10 @@ Discourse.AdminUserBadgesController = Ember.ArrayController.extend({ self.pushObject(userBadge); Ember.run.next(function() { // Update the selected badge ID after the combobox has re-rendered. - self.set('selectedBadgeId', self.get('grantableBadges')[0].get('id')); + var newSelectedBadge = self.get('grantableBadges')[0]; + if (newSelectedBadge) { + self.set('selectedBadgeId', newSelectedBadge.get('id')); + } }); }, function() { // Failure diff --git a/app/assets/javascripts/discourse/controllers/poster_expansion_controller.js b/app/assets/javascripts/discourse/controllers/poster_expansion_controller.js index ae3959f778d..99cca8f5572 100644 --- a/app/assets/javascripts/discourse/controllers/poster_expansion_controller.js +++ b/app/assets/javascripts/discourse/controllers/poster_expansion_controller.js @@ -20,6 +20,16 @@ Discourse.PosterExpansionController = Discourse.ObjectController.extend({ hasUserFilters: Em.computed.gt('postStream.userFilters.length', 0), + showBadges: function() { + return Discourse.SiteSettings.enable_badges; + }.property(), + + moreBadgesCount: function() { + return this.get('user.badge_count') - this.get('user.featured_user_badges.length'); + }.property('user.badge_count', 'user.featured_user_badges.@each'), + + showMoreBadges: Em.computed.gt('moreBadgesCount', 0), + show: function(post) { // Don't show on mobile diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js index bc5186dd68f..fc78724871a 100644 --- a/app/assets/javascripts/discourse/models/user.js +++ b/app/assets/javascripts/discourse/models/user.js @@ -312,10 +312,21 @@ Discourse.User = Discourse.Model.extend({ return Discourse.Group.create(g); }); } + if (json.user.invited_by) { json.user.invited_by = Discourse.User.create(json.user.invited_by); } + if (!Em.isEmpty(json.user.featured_user_badge_ids)) { + var userBadgesMap = {}; + Discourse.UserBadge.createFromJson(json).forEach(function(userBadge) { + userBadgesMap[ userBadge.get('id') ] = userBadge; + }); + json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) { + return userBadgesMap[id]; + }); + } + user.setProperties(json.user); return user; }); diff --git a/app/assets/javascripts/discourse/templates/poster_expansion.handlebars b/app/assets/javascripts/discourse/templates/poster_expansion.handlebars index d07cf38c780..12957c1e43a 100644 --- a/app/assets/javascripts/discourse/templates/poster_expansion.handlebars +++ b/app/assets/javascripts/discourse/templates/poster_expansion.handlebars @@ -1,16 +1,31 @@ {{#if model}} {{#link-to 'user' user}}{{boundAvatar model imageSize="huge"}}{{/link-to}} -

{{username}}

- {{#if showName}} -

{{name}}

- {{/if}} +
+

{{username}}

+ {{#if showName}} +

{{name}}

+ {{/if}} +
{{#if user}} -

{{i18n last_post}} {{unboundDate path="user.last_posted_at" leaveAgo="true"}}

-

{{i18n joined}} {{unboundDate path="user.created_at" leaveAgo="true"}}

+
+

{{i18n last_post}} {{unboundDate path="user.last_posted_at" leaveAgo="true"}}

+

{{i18n joined}} {{unboundDate path="user.created_at" leaveAgo="true"}}

+ {{groups-list groups=user.custom_groups}} +
- {{groups-list groups=user.custom_groups}} + {{#if showBadges}} +
+

{{i18n badges.badge_count count=user.badge_count}}

+ {{#each user.featured_user_badges}} + {{badge.name}} + {{/each}} + {{#if showMoreBadges}} + {{i18n badges.more_badges count=moreBadgesCount}} + {{/if}} +
+ {{/if}}
{{#if user.bio_cooked}}
{{{user.bio_cooked}}}
{{/if}} diff --git a/app/assets/javascripts/discourse/views/poster_expansion_view.js b/app/assets/javascripts/discourse/views/poster_expansion_view.js index 62c175246fe..c2bc872e70d 100644 --- a/app/assets/javascripts/discourse/views/poster_expansion_view.js +++ b/app/assets/javascripts/discourse/views/poster_expansion_view.js @@ -10,7 +10,7 @@ var clickOutsideEventName = "mousedown.outside-poster-expansion"; Discourse.PosterExpansionView = Discourse.View.extend({ elementId: 'poster-expansion', - classNameBindings: ['controller.visible::hidden'], + classNameBindings: ['controller.visible::hidden', 'controller.showBadges'], // Position the expansion when the post changes _visibleChanged: function() { diff --git a/app/assets/stylesheets/desktop/poster_expansion.scss b/app/assets/stylesheets/desktop/poster_expansion.scss index 066bc3d9e0e..bbd1fb9e20d 100644 --- a/app/assets/stylesheets/desktop/poster_expansion.scss +++ b/app/assets/stylesheets/desktop/poster_expansion.scss @@ -65,7 +65,7 @@ } p.loading { - margin-top: 30px; + margin-top: 45px; color: $secondary_text_color; } @@ -76,4 +76,47 @@ .new-user a { color: $secondary_text_color; } + + &.show-badges { + width: 560px; + + .names { + width: 250px; + float: left; + } + + .metadata { + float: right; + max-width: 180px; + } + + h2 { + line-height: 15px; + } + + .badge-section { + h3 { + line-height: 43px; + color: $primary_text_color; + font-size: 14px; + margin-bottom: -8px; + } + + .badge, .more-badges { + font-size: 12px; + margin: 0; + line-height: 16px; + display: inline-block; + } + + .badge { + padding: 3px 8px; + border: 1px solid $secondary-border-color; + } + + .more-badges { + padding: 4px 8px; + } + } + } } diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index cdd25726af6..1431ccd9f8b 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -21,10 +21,12 @@ class UserSerializer < BasicUserSerializer :admin, :title, :suspend_reason, - :suspended_till + :suspended_till, + :badge_count has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_many :custom_groups, embed: :object, serializer: BasicGroupSerializer + has_many :featured_user_badges, embed: :ids, serializer: UserBadgeSerializer, root: :user_badges def self.private_attributes(*attrs) attributes(*attrs) @@ -128,4 +130,13 @@ class UserSerializer < BasicUserSerializer def watched_category_ids 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/config/locales/client.en.yml b/config/locales/client.en.yml index e2d44f9853f..044d2b17472 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1765,6 +1765,12 @@ en: mark_watching: 'm then w Mark topic as watching' badges: + badge_count: + one: "1 Badge" + other: "%{count} Badges" + more_badges: + one: "+1 More" + other: "+%{count} More" example_badge: name: Example Badge description: This is a generic example badge. diff --git a/lib/freedom_patches/ams_include_without_root.rb b/lib/freedom_patches/ams_include_without_root.rb new file mode 100644 index 00000000000..424489b3f5c --- /dev/null +++ b/lib/freedom_patches/ams_include_without_root.rb @@ -0,0 +1,59 @@ +# Just ignore included associations that are to be embedded in the root instead of +# throwing an exception in AMS 0.8.x. +# +# The 0.9.0 branch does exactly this, see: +# https://github.com/rails-api/active_model_serializers/issues/377 +module ActiveModel + class Serializer + # This method is copied over verbatim from the AMS version, except for silently + # ignoring associations that cannot be embedded without a root instead of + # raising an exception. + def include!(name, options={}) + unique_values = + if hash = options[:hash] + if @options[:hash] == hash + @options[:unique_values] ||= {} + else + {} + end + else + hash = @options[:hash] + @options[:unique_values] ||= {} + end + + node = options[:node] ||= @node + value = options[:value] + + if options[:include] == nil + if @options.key?(:include) + options[:include] = @options[:include].include?(name) + elsif @options.include?(:exclude) + options[:include] = !@options[:exclude].include?(name) + end + end + + association_class = + if klass = _associations[name] + klass + elsif value.respond_to?(:to_ary) + Associations::HasMany + else + Associations::HasOne + end + + association = association_class.new(name, self, options) + + if association.embed_ids? + node[association.key] = association.serialize_ids + + if association.embed_in_root? && hash.nil? + # Don't raise an error! + elsif association.embed_in_root? && association.embeddable? + merge_association hash, association.root, association.serializables, unique_values + end + elsif association.embed_objects? + node[association.key] = association.serialize + end + end + end +end