mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-02-17 04:01:29 -05:00
Merge pull request #2276 from vikhyat/badge-system
Badge system updates
This commit is contained in:
commit
756ea0178a
18 changed files with 199 additions and 15 deletions
|
@ -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'));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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};
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue