mirror of
https://github.com/discourse/discourse.git
synced 2025-04-01 22:06:45 +08:00
Merge pull request #2276 from vikhyat/badge-system
Badge system updates
This commit is contained in:
commit
756ea0178a
@ -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;
|
return Discourse.SiteSettings.enable_names;
|
||||||
}.property(),
|
}.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() {
|
availableLocales: function() {
|
||||||
return Discourse.SiteSettings.available_locales.split('|').map( function(s) {
|
return Discourse.SiteSettings.available_locales.split('|').map( function(s) {
|
||||||
return {name: s, value: s};
|
return {name: s, value: s};
|
||||||
|
@ -19,8 +19,8 @@ Discourse.UserController = Discourse.ObjectController.extend({
|
|||||||
}.property('viewingSelf'),
|
}.property('viewingSelf'),
|
||||||
|
|
||||||
showBadges: function() {
|
showBadges: function() {
|
||||||
return Discourse.SiteSettings.enable_badges;
|
return Discourse.SiteSettings.enable_badges && (this.get('content.badge_count') > 0);
|
||||||
}.property(),
|
}.property('content.badge_count'),
|
||||||
|
|
||||||
privateMessageView: function() {
|
privateMessageView: function() {
|
||||||
return (this.get('userActionType') === Discourse.UserAction.TYPES.messages_sent) ||
|
return (this.get('userActionType') === Discourse.UserAction.TYPES.messages_sent) ||
|
||||||
|
@ -81,6 +81,7 @@ Discourse.Route.buildRoutes(function() {
|
|||||||
this.route('username');
|
this.route('username');
|
||||||
this.route('email');
|
this.route('email');
|
||||||
this.route('about', { path: '/about-me' });
|
this.route('about', { path: '/about-me' });
|
||||||
|
this.route('badgeTitle', { path: '/badge_title' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('invited');
|
this.route('invited');
|
||||||
|
@ -18,6 +18,7 @@ Discourse.BadgesShowRoute = Ember.Route.extend({
|
|||||||
setupController: function(controller, model) {
|
setupController: function(controller, model) {
|
||||||
Discourse.UserBadge.findByBadgeId(model.get('id')).then(function(userBadges) {
|
Discourse.UserBadge.findByBadgeId(model.get('id')).then(function(userBadges) {
|
||||||
controller.set('userBadges', userBadges);
|
controller.set('userBadges', userBadges);
|
||||||
|
controller.set('userBadgesLoaded', true);
|
||||||
});
|
});
|
||||||
controller.set('model', model);
|
controller.set('model', model);
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
|
|||||||
user.set('avatar_template', avatarSelector.get('avatarTemplate'));
|
user.set('avatar_template', avatarSelector.get('avatarTemplate'));
|
||||||
avatarSelector.send('closeModal');
|
avatarSelector.send('closeModal');
|
||||||
},
|
},
|
||||||
|
|
||||||
showProfileBackgroundFileSelector: function() {
|
showProfileBackgroundFileSelector: function() {
|
||||||
$("#profile-background-input").click();
|
$("#profile-background-input").click();
|
||||||
},
|
},
|
||||||
@ -161,3 +161,43 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
|
|||||||
controller.setProperties({ model: user, newUsername: user.get('username') });
|
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>
|
<tr>
|
||||||
<td class='badge'>{{user-badge badge=this}}</td>
|
<td class='badge'>{{user-badge badge=this}}</td>
|
||||||
<td class='description'>{{description}}</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>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class='badge'>{{user-badge badge=this}}</td>
|
<td class='badge'>{{user-badge badge=this}}</td>
|
||||||
<td class='description'>{{description}}</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>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@ -22,6 +22,8 @@
|
|||||||
{{/link-to}}
|
{{/link-to}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class='spinner'>{{i18n loading}}</div>
|
{{#unless userBadgesLoaded}}
|
||||||
|
<div class='spinner'>{{i18n loading}}</div>
|
||||||
|
{{/unless}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,9 @@
|
|||||||
{{user-badge badge=badge}}
|
{{user-badge badge=badge}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if showMoreBadges}}
|
{{#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}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/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>
|
</div>
|
||||||
{{/if}}
|
{{/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">
|
<div class="control-group">
|
||||||
<label class="control-label">{{i18n user.email.title}}</label>
|
<label class="control-label">{{i18n user.email.title}}</label>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
|
@ -63,6 +63,7 @@ table.badges-listing {
|
|||||||
td.grant-count {
|
td.grant-count {
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: $secondary_text_color;
|
color: $secondary_text_color;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
td.badge, td.grant-count {
|
td.badge, td.grant-count {
|
||||||
|
@ -61,6 +61,21 @@ class UsersController < ApplicationController
|
|||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
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
|
def preferences
|
||||||
render nothing: true
|
render nothing: true
|
||||||
end
|
end
|
||||||
|
@ -40,8 +40,15 @@ class BadgeGranter
|
|||||||
if options[:revoked_by]
|
if options[:revoked_by]
|
||||||
StaffActionLogger.new(options[:revoked_by]).log_badge_revoke(user_badge)
|
StaffActionLogger.new(options[:revoked_by]).log_badge_revoke(user_badge)
|
||||||
end
|
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 = 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
|
notification && notification.destroy
|
||||||
end
|
end
|
||||||
|
@ -598,7 +598,7 @@ en:
|
|||||||
moved_post: "<i title='moved post' class='fa fa-arrow-right'></i> {{username}} moved {{link}}"
|
moved_post: "<i title='moved post' class='fa fa-arrow-right'></i> {{username}} moved {{link}}"
|
||||||
total_flagged: "total flagged posts"
|
total_flagged: "total flagged posts"
|
||||||
linked: "<i title='linked post' class='fa fa-arrow-left'></i> {{username}} {{link}}"
|
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:
|
upload_selector:
|
||||||
title: "Add an image"
|
title: "Add an image"
|
||||||
@ -1776,9 +1776,10 @@ en:
|
|||||||
more_badges:
|
more_badges:
|
||||||
one: "+1 More"
|
one: "+1 More"
|
||||||
other: "+%{count} More"
|
other: "+%{count} More"
|
||||||
awarded:
|
granted:
|
||||||
one: "1 awarded"
|
one: "1 granted"
|
||||||
other: "%{count} awarded"
|
other: "%{count} granted"
|
||||||
|
select_badge_for_title: Select a badge to use as your title
|
||||||
example_badge:
|
example_badge:
|
||||||
name: Example Badge
|
name: Example Badge
|
||||||
description: This is a generic 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}"
|
invited_to_private_message: "%{display_username} invited you to a private message: %{link}"
|
||||||
invitee_accepted: "%{display_username} accepted your invitation"
|
invitee_accepted: "%{display_username} accepted your invitation"
|
||||||
linked: "%{display_username} linked you in %{link}"
|
linked: "%{display_username} linked you in %{link}"
|
||||||
granted_badge: "You were granted the badge %{link}"
|
granted_badge: "You were granted %{link}"
|
||||||
|
|
||||||
search:
|
search:
|
||||||
within_post: "#%{post_number} by %{username}: %{excerpt}"
|
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}
|
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}
|
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/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}
|
get "users/:username/preferences/username" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||||
put "users/:username/preferences/username" => "users#username", 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
|
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(:admin) { Fabricate(:admin) }
|
||||||
let!(:user_badge) { BadgeGranter.grant(badge, user) }
|
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)
|
badge.reload.grant_count.should eq(1)
|
||||||
StaffActionLogger.any_instance.expects(:log_badge_revoke).with(user_badge)
|
StaffActionLogger.any_instance.expects(:log_badge_revoke).with(user_badge)
|
||||||
BadgeGranter.revoke(user_badge, revoked_by: admin)
|
BadgeGranter.revoke(user_badge, revoked_by: admin)
|
||||||
UserBadge.where(user: user, badge: badge).first.should_not be_present
|
UserBadge.where(user: user, badge: badge).first.should_not be_present
|
||||||
badge.reload.grant_count.should eq(0)
|
badge.reload.grant_count.should eq(0)
|
||||||
user.notifications.where(notification_type: Notification.types[:granted_badge]).should be_empty
|
user.notifications.where(notification_type: Notification.types[:granted_badge]).should be_empty
|
||||||
|
user.reload.title.should == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user