Merge pull request #2276 from vikhyat/badge-system

Badge system updates
This commit is contained in:
Régis Hanol 2014-04-18 11:46:19 +02:00
commit 756ea0178a
18 changed files with 199 additions and 15 deletions

View file

@ -0,0 +1,64 @@
/**
Controller for selecting a badge to use as your title.
@class PreferencesBadgeTitleController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesBadgeTitleController = Ember.ArrayController.extend({
saving: false,
saved: false,
savingStatus: function() {
if (this.get('saving')) {
return I18n.t('saving');
} else {
return I18n.t('save');
}
}.property('saving'),
selectableUserBadges: Em.computed.filter('model', function(userBadge) {
var badgeType = userBadge.get('badge.badge_type.name');
return (badgeType === "Gold" || badgeType === "Silver");
}),
selectedUserBadge: function() {
var selectedUserBadgeId = parseInt(this.get('selectedUserBadgeId'));
var selectedUserBadge = null;
this.get('selectableUserBadges').forEach(function(userBadge) {
if (userBadge.get('id') === selectedUserBadgeId) {
selectedUserBadge = userBadge;
}
});
return selectedUserBadge;
}.property('selectedUserBadgeId'),
titleNotChanged: function() {
return this.get('user.title') === this.get('selectedUserBadge.badge.name');
}.property('selectedUserBadge', 'user.title'),
disableSave: Em.computed.or('saving', 'titleNotChanged'),
actions: {
save: function() {
var self = this;
self.set('saved', false);
self.set('saving', true);
Discourse.ajax("/users/" + self.get('user.username_lower') + "/preferences/badge_title", {
type: "PUT",
data: {
user_badge_id: self.get('selectedUserBadgeId')
}
}).then(function() {
self.set('saved', true);
self.set('saving', false);
self.set('user.title', self.get('selectedUserBadge.badge.name'));
}, function() {
bootbox.alert(I18n.t('generic_error'));
});
}
}
});

View file

@ -38,6 +38,17 @@ Discourse.PreferencesController = Discourse.ObjectController.extend({
return Discourse.SiteSettings.enable_names;
}.property(),
canSelectTitle: function() {
if (!Discourse.SiteSettings.enable_badges || this.get('model.badge_count') === 0) {
return false;
}
// If the first featured badge isn't gold or silver we know the user won't have
// _any_ gold or silver badges.
var badgeType = this.get('model.featured_user_badges')[0].get('badge.badge_type.name');
return (badgeType === "Gold" || badgeType === "Silver");
}.property('model.badge_count', 'model.featured_user_badges.@each.badge.badge_type.name'),
availableLocales: function() {
return Discourse.SiteSettings.available_locales.split('|').map( function(s) {
return {name: s, value: s};

View file

@ -19,8 +19,8 @@ Discourse.UserController = Discourse.ObjectController.extend({
}.property('viewingSelf'),
showBadges: function() {
return Discourse.SiteSettings.enable_badges;
}.property(),
return Discourse.SiteSettings.enable_badges && (this.get('content.badge_count') > 0);
}.property('content.badge_count'),
privateMessageView: function() {
return (this.get('userActionType') === Discourse.UserAction.TYPES.messages_sent) ||

View file

@ -81,6 +81,7 @@ Discourse.Route.buildRoutes(function() {
this.route('username');
this.route('email');
this.route('about', { path: '/about-me' });
this.route('badgeTitle', { path: '/badge_title' });
});
this.route('invited');

View file

@ -18,6 +18,7 @@ Discourse.BadgesShowRoute = Ember.Route.extend({
setupController: function(controller, model) {
Discourse.UserBadge.findByBadgeId(model.get('id')).then(function(userBadges) {
controller.set('userBadges', userBadges);
controller.set('userBadgesLoaded', true);
});
controller.set('model', model);
}

View file

@ -44,7 +44,7 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
user.set('avatar_template', avatarSelector.get('avatarTemplate'));
avatarSelector.send('closeModal');
},
showProfileBackgroundFileSelector: function() {
$("#profile-background-input").click();
},
@ -161,3 +161,43 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
controller.setProperties({ model: user, newUsername: user.get('username') });
}
});
/**
The route for updating a user's title to one of their badges
@class PreferencesBadgeTitleRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesBadgeTitleRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username'));
},
renderTemplate: function() {
return this.render('user/badge-title', { into: 'user', outlet: 'userOutlet' });
},
// A bit odd, but if we leave to /preferences we need to re-render that outlet
deactivate: function() {
this._super();
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
},
setupController: function(controller, model) {
controller.set('model', model);
controller.set('user', this.modelFor('user'));
model.forEach(function(userBadge) {
if (userBadge.get('badge.name') === controller.get('user.title')) {
controller.set('selectedUserBadgeId', userBadge.get('id'));
}
});
if (!controller.get('selectedUserBadgeId')) {
controller.set('selectedUserBadgeId', controller.get('selectableUserBadges')[0].get('id'));
}
}
});

View file

@ -6,7 +6,7 @@
<tr>
<td class='badge'>{{user-badge badge=this}}</td>
<td class='description'>{{description}}</td>
<td class='grant-count'>{{i18n badges.awarded count=grant_count}}</td>
<td class='grant-count'>{{i18n badges.granted count=grant_count}}</td>
</tr>
{{/each}}
</table>

View file

@ -9,7 +9,7 @@
<tr>
<td class='badge'>{{user-badge badge=this}}</td>
<td class='description'>{{description}}</td>
<td class='grant-count'>{{i18n badges.awarded count=grant_count}}</td>
<td class='grant-count'>{{i18n badges.granted count=grant_count}}</td>
</tr>
</table>
@ -22,6 +22,8 @@
{{/link-to}}
{{/each}}
{{else}}
<div class='spinner'>{{i18n loading}}</div>
{{#unless userBadgesLoaded}}
<div class='spinner'>{{i18n loading}}</div>
{{/unless}}
{{/if}}
</div>

View file

@ -13,7 +13,9 @@
{{user-badge badge=badge}}
{{/each}}
{{#if showMoreBadges}}
<span class="btn more-user-badges">{{i18n badges.more_badges count=moreBadgesCount}}</span>
{{#link-to 'user.badges' user class="btn more-user-badges"}}
{{i18n badges.more_badges count=moreBadgesCount}}
{{/link-to}}
{{/if}}
</div>
{{/if}}

View file

@ -0,0 +1,25 @@
<section class='user-content'>
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<h3>{{i18n badges.select_badge_for_title}}</h3>
</div>
</div>
<div class="control-group">
<label class="control-label">{{i18n badges.title}}</label>
<div class="controls">
{{combobox valueAttribute="id" value=selectedUserBadgeId nameProperty="badge.name" content=selectableUserBadges}}
</div>
</div>
<div class="control-group">
<div class="controls">
<button class="btn btn-primary" {{bind-attr disabled=disableSave}} {{action save}}>{{savingStatus}}</button>
{{#if saved}}{{i18n saved}}{{/if}}
</div>
</div>
</form>
</section>

View file

@ -37,6 +37,16 @@
</div>
{{/if}}
{{#if canSelectTitle}}
<div class="control-group">
<label class="control-label">{{i18n user.title.title}}</label>
<div class="controls">
<span class="static">{{title}}</span>
{{#link-to "preferences.badgeTitle" class="btn pad-left"}}<i class="fa fa-pencil"></i>{{/link-to}}
</div>
</div>
{{/if}}
<div class="control-group">
<label class="control-label">{{i18n user.email.title}}</label>
<div class="controls">

View file

@ -63,6 +63,7 @@ table.badges-listing {
td.grant-count {
font-size: 0.8em;
color: $secondary_text_color;
text-align: right;
}
td.badge, td.grant-count {

View file

@ -61,6 +61,21 @@ class UsersController < ApplicationController
render nothing: true
end
def badge_title
params.require(:user_badge_id)
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
user_badge = UserBadge.find(params[:user_badge_id])
if user_badge.user == user && ["Gold", "Silver"].include?(user_badge.badge.badge_type.name)
user.title = user_badge.badge.name
user.save!
end
render nothing: true
end
def preferences
render nothing: true
end

View file

@ -40,8 +40,15 @@ class BadgeGranter
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.
# If the user's title is the same as the badge name, remove their title.
if user_badge.user.title == user_badge.badge.name
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

View file

@ -598,7 +598,7 @@ en:
moved_post: "<i title='moved post' class='fa fa-arrow-right'></i> {{username}} moved {{link}}"
total_flagged: "total flagged posts"
linked: "<i title='linked post' class='fa fa-arrow-left'></i> {{username}} {{link}}"
granted_badge: "<i title='badge granted' class='fa fa-certificate'></i> {{link}}"
granted_badge: "<i title='badge granted' class='fa fa-certificate'></i> You were granted {{link}}"
upload_selector:
title: "Add an image"
@ -1776,9 +1776,10 @@ en:
more_badges:
one: "+1 More"
other: "+%{count} More"
awarded:
one: "1 awarded"
other: "%{count} awarded"
granted:
one: "1 granted"
other: "%{count} granted"
select_badge_for_title: Select a badge to use as your title
example_badge:
name: Example Badge
description: This is a generic example badge.

View file

@ -888,7 +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}"
granted_badge: "You were granted %{link}"
search:
within_post: "#%{post_number} by %{username}: %{excerpt}"

View file

@ -184,6 +184,8 @@ Discourse::Application.routes.draw do
get "users/:username/preferences/email" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/email" => "users#change_email", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/preferences/about-me" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/preferences/badge_title" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/badge_title" => "users#badge_title", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/preferences/username" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/username" => "users#username", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/avatar(/:size)" => "users#avatar", constraints: {username: USERNAME_ROUTE_FORMAT} # LEGACY ROUTE

View file

@ -53,13 +53,15 @@ describe BadgeGranter do
let(:admin) { Fabricate(:admin) }
let!(:user_badge) { BadgeGranter.grant(badge, user) }
it 'revokes the badge, deletes the notification and decrements grant_count' do
it 'revokes the badge and does necessary cleanup' do
user.title = badge.name; user.save!
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
user.reload.title.should == nil
end
end