FEATURE: Allow users to select a badge with an image to appear on their

user card
This commit is contained in:
Robin Ward 2014-10-20 13:15:58 -04:00
parent d2fb2bc4cd
commit 71f211f0b3
31 changed files with 432 additions and 279 deletions

View File

@ -26,7 +26,7 @@ export default Ember.ObjectController.extend(BufferedContent, {
'listable', 'auto_revoke',
'enabled', 'show_posts',
'target_posts', 'name', 'description',
'icon', 'query', 'badge_grouping_id',
'icon', 'image', 'query', 'badge_grouping_id',
'trigger', 'badge_type_id'],
self = this;

View File

@ -18,6 +18,12 @@
<p class='help'>{{i18n admin.badges.icon_help}}</p>
</div>
<div>
<label for="name">{{i18n admin.badges.image}}</label>
{{input type="text" name="name" value=buffered.image}}
<p class='help'>{{i18n admin.badges.icon_help}}</p>
</div>
<div>
<label for="badge_type_id">{{i18n admin.badges.badge_type}}</label>
{{view Ember.Select name="badge_type_id"

View File

@ -3,7 +3,5 @@ export default Ember.Component.extend({
classNameBindings: [':user-badge', 'badge.badgeTypeClassName'],
title: Em.computed.alias('badge.displayDescription'),
attributeBindings: ['data-badge-name', 'title'],
'data-badge-name': Em.computed.alias('badge.name'),
isIcon: Em.computed.match('badge.icon', /^fa-/)
'data-badge-name': Em.computed.alias('badge.name')
});

View File

@ -1,63 +1,25 @@
/**
Controller for selecting a badge to use as your title.
import BadgeSelectController from "discourse/mixins/badge-select-controller";
@class PreferencesBadgeTitleController
@extends Ember.ArrayController
@namespace Discourse
@module Discourse
**/
export default Ember.ArrayController.extend({
saving: false,
saved: false,
export default Ember.ArrayController.extend(BadgeSelectController, {
savingStatus: function() {
if (this.get('saving')) {
return I18n.t('saving');
} else {
return I18n.t('save');
}
}.property('saving'),
selectableUserBadges: function() {
var items = this.get('model').filterBy('badge.allow_title', true);
items = _.uniq(items, false, function(e) { return e.get('badge.name'); });
items.unshiftObject(Em.Object.create({
badge: Discourse.Badge.create({name: I18n.t('badges.no_title')})
}));
return items;
filteredList: function() {
return this.get('model').filterBy('badge.allow_title', true);
}.property('model'),
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: Discourse.computed.propertyEqual('user.title', 'selectedUserBadge.badge.name'),
disableSave: Em.computed.or('saving', 'titleNotChanged'),
actions: {
save: function() {
this.setProperties({ saved: false, saving: true });
var self = this;
self.set('saved', false);
self.set('saving', true);
Discourse.ajax("/users/" + self.get('user.username_lower') + "/preferences/badge_title", {
Discourse.ajax(this.get('user.path') + "/preferences/badge_title", {
type: "PUT",
data: {
user_badge_id: self.get('selectedUserBadgeId')
}
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'));
self.setProperties({
saved: true,
saving: false,
"user.title": self.get('selectedUserBadge.badge.name')
});
}, function() {
bootbox.alert(I18n.t('generic_error'));
});

View File

@ -0,0 +1,30 @@
import BadgeSelectController from "discourse/mixins/badge-select-controller";
export default Ember.ArrayController.extend(BadgeSelectController, {
filteredList: function() {
return this.get('model').filter(function(b) {
return !Em.empty(b.get('badge.image'));
});
}.property('model'),
actions: {
save: function() {
this.setProperties({ saved: false, saving: true });
var self = this;
Discourse.ajax(this.get('user.path') + "/preferences/card-badge", {
type: "PUT",
data: { user_badge_id: self.get('selectedUserBadgeId') }
}).then(function() {
self.setProperties({
saved: true,
saving: false,
"user.card_image_badge": self.get('selectedUserBadge.badge.image')
});
}).catch(function() {
self.set('saving', false);
bootbox.alert(I18n.t('generic_error'));
});
}
}
});

View File

@ -0,0 +1,9 @@
export default Ember.Handlebars.makeBoundHelper(function(str) {
if (Em.empty(str)) { return ""; }
if (str.indexOf('fa-') === 0) {
return new Handlebars.SafeString("<i class='fa " + str + "'></i>");
} else {
return new Handlebars.SafeString("<img src='" + str + "'>");
}
});

View File

@ -0,0 +1,35 @@
export default Ember.Mixin.create({
saving: false,
saved: false,
selectableUserBadges: function() {
var items = this.get('filteredList');
items = _.uniq(items, false, function(e) { return e.get('badge.name'); });
items.unshiftObject(Em.Object.create({
badge: Discourse.Badge.create({name: I18n.t('badges.none')})
}));
return items;
}.property('filteredList'),
savingStatus: function() {
if (this.get('saving')) {
return I18n.t('saving');
} else {
return I18n.t('save');
}
}.property('saving'),
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'),
disableSave: Em.computed.alias('saving')
});

View File

@ -338,6 +338,10 @@ Discourse.User = Discourse.Model.extend({
});
}
if (json.user.card_badge) {
json.user.card_badge = Discourse.Badge.create(json.user.card_badge);
}
user.setProperties(json.user);
return user;
});

View File

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

View File

@ -0,0 +1,38 @@
export default Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newBio: model.get('bio_raw') });
},
// 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' });
},
actions: {
changeAbout: function() {
var route = this;
var controller = route.controllerFor('preferences/about');
controller.setProperties({ saving: true });
return controller.get('model').save().then(function() {
controller.set('saving', false);
route.transitionTo('user.index');
}, function() {
// model failed to save
controller.set('saving', false);
alert(I18n.t('generic_error'));
});
}
}
});

View File

@ -0,0 +1,29 @@
export default 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.get('selectableUserBadges.length') > 0) {
controller.set('selectedUserBadgeId', controller.get('selectableUserBadges')[0].get('id'));
}
}
});

View File

@ -0,0 +1,27 @@
export default Discourse.RestrictedUserRoute.extend({
model: function() {
return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username'));
},
renderTemplate: function() {
return this.render({ 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.image') === controller.get('user.card_image_badge')) {
controller.set('selectedUserBadgeId', userBadge.get('id'));
}
});
}
});

View File

@ -0,0 +1,20 @@
export default Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newEmail: model.get('email') });
},
// 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' });
}
});

View File

@ -0,0 +1,5 @@
export default Discourse.RestrictedUserRoute.extend({
renderTemplate: function() {
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
}
});

View File

@ -0,0 +1,19 @@
export default Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
return this.render({ 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, user) {
controller.setProperties({ model: user, newUsername: user.get('username') });
}
});

View File

@ -0,0 +1,59 @@
export default Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
setupController: function(controller, user) {
controller.setProperties({ model: user, newNameInput: user.get('name') });
this.controllerFor('user').set('indexStream', false);
},
actions: {
showAvatarSelector: function() {
Discourse.Route.showModal(this, 'avatar-selector');
// all the properties needed for displaying the avatar selector modal
var controller = this.controllerFor('avatar-selector');
var user = this.modelFor('user');
var props = user.getProperties(
'username', 'email',
'uploaded_avatar_id',
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
);
switch(props.uploaded_avatar_id){
case props.system_avatar_upload_id:
props.selected = "system";
break;
case props.gravatar_avatar_upload_id:
props.selected = "gravatar";
break;
default:
props.selected = "uploaded";
}
controller.setProperties(props);
},
saveAvatarSelection: function() {
var user = this.modelFor('user');
var avatarSelector = this.controllerFor('avatar-selector');
// sends the information to the server if it has changed
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) {
user.pickAvatar(avatarSelector.get('selectedUploadId'));
}
// saves the data back
user.setProperties(avatarSelector.getProperties(
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
avatarSelector.send('closeModal');
},
}
});

View File

@ -1,215 +0,0 @@
/**
The common route stuff for a user's preference
@class PreferencesRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
setupController: function(controller, user) {
controller.setProperties({ model: user, newNameInput: user.get('name') });
this.controllerFor('user').set('indexStream', false);
},
actions: {
showAvatarSelector: function() {
Discourse.Route.showModal(this, 'avatar-selector');
// all the properties needed for displaying the avatar selector modal
var controller = this.controllerFor('avatar-selector');
var user = this.modelFor('user');
var props = user.getProperties(
'username', 'email',
'uploaded_avatar_id',
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
);
switch(props.uploaded_avatar_id){
case props.system_avatar_upload_id:
props.selected = "system";
break;
case props.gravatar_avatar_upload_id:
props.selected = "gravatar";
break;
default:
props.selected = "uploaded";
}
controller.setProperties(props);
},
saveAvatarSelection: function() {
var user = this.modelFor('user');
var avatarSelector = this.controllerFor('avatar-selector');
// sends the information to the server if it has changed
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) {
user.pickAvatar(avatarSelector.get('selectedUploadId'));
}
// saves the data back
user.setProperties(avatarSelector.getProperties(
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
avatarSelector.send('closeModal');
},
}
});
Discourse.PreferencesIndexRoute = Discourse.RestrictedUserRoute.extend({
renderTemplate: function() {
this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
}
});
/**
The route for editing a user's "About Me" bio.
@class PreferencesAboutRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesAboutRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newBio: model.get('bio_raw') });
},
// 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' });
},
actions: {
changeAbout: function() {
var route = this;
var controller = route.controllerFor('preferences/about');
controller.setProperties({ saving: true });
return controller.get('model').save().then(function() {
controller.set('saving', false);
route.transitionTo('user.index');
}, function() {
// model failed to save
controller.set('saving', false);
alert(I18n.t('generic_error'));
});
}
}
});
/**
The route for editing a user's email
@class PreferencesEmailRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesEmailRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
this.render({ into: 'user', outlet: 'userOutlet' });
},
setupController: function(controller, model) {
controller.setProperties({ model: model, newEmail: model.get('email') });
},
// 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' });
}
});
/**
The route for updating a user's username
@class PreferencesUsernameRoute
@extends Discourse.RestrictedUserRoute
@namespace Discourse
@module Discourse
**/
Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
model: function() {
return this.modelFor('user');
},
renderTemplate: function() {
return this.render({ 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, user) {
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.get('selectableUserBadges.length') > 0) {
controller.set('selectedUserBadgeId', controller.get('selectableUserBadges')[0].get('id'));
}
}
});

View File

@ -1,7 +1,3 @@
{{#if isIcon}}
<i {{bind-attr class=":fa badge.icon"}}></i>
{{else}}
<img {{bind-attr src="badge.icon"}}>
{{/if}}
{{icon-or-image badge.icon}}
{{badge.displayName}}
{{yield}}

View File

@ -0,0 +1,25 @@
<section class='user-content'>
<form class="form-horizontal">
<div class="control-group">
<div class="controls">
<h3>{{i18n user.card_badge.title}}</h3>
</div>
</div>
<div class="control-group">
<label class="control-label"></label>
<div class="controls">
{{combo-box valueAttribute="id" value=selectedUserBadgeId nameProperty="badge.displayName" 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

@ -55,6 +55,12 @@
{{#if hasUserFilters}}
<button class='btn' {{action "cancelFilter"}}>{{fa-icon "times"}}{{i18n topic.filters.cancel}}</button>
{{/if}}
{{#if user.card_badge}}
{{#link-to 'badges.show' user.card_badge class="card-badge"}}
{{icon-or-image user.card_badge.image}}
{{/link-to}}
{{/if}}
</div>
{{else}}
<p class='loading'>{{i18n loading}}</p>

View File

@ -42,7 +42,7 @@
<label class="control-label">{{i18n user.title.title}}</label>
<div class="controls">
<span class="static">{{title}}</span>
{{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}<i class="fa fa-pencil"></i>{{/link-to}}
{{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
</div>
</div>
{{/if}}
@ -157,6 +157,16 @@
</div>
</div>
<div class="control-group pref-card-badge">
<label class="control-label">{{i18n user.card_badge.title}}</label>
<div class="controls">
{{#if card_image_badge}}
{{icon-or-image card_image_badge}}
{{/if}}
{{#link-to "preferences.card-badge" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
</div>
</div>
<div class="control-group pref-email-settings">
<label class="control-label">{{i18n user.email_settings}}</label>
{{preference-checkbox labelKey="user.email_digests.title" checked=email_digests}}

View File

@ -145,4 +145,12 @@
color: $danger;
margin-bottom: 10px;
}
.card-badge {
img {
max-width: 100px;
}
float: right;
margin-right: 5px;
}
}

View File

@ -39,6 +39,23 @@ class UsersController < ApplicationController
end
end
def card_badge
end
def update_card_badge
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
user_badge = UserBadge.find_by(id: params[:user_badge_id].to_i)
if user_badge && user_badge.user == user && user_badge.badge.image.present?
user.user_profile.update_column(:card_image_badge_id, user_badge.badge.id)
else
user.user_profile.update_column(:card_image_badge_id, nil)
end
render nothing: true
end
def user_preferences_redirect
redirect_to email_preferences_path(current_user.username_lower)
end

View File

@ -6,6 +6,8 @@ class UserProfile < ActiveRecord::Base
before_save :cook
after_save :trigger_badges
belongs_to :card_image_badge, class_name: 'Badge'
BAKED_VERSION = 1
def bio_excerpt

View File

@ -1,6 +1,6 @@
class BadgeSerializer < ApplicationSerializer
attributes :id, :name, :description, :grant_count, :allow_title,
:multiple_grant, :icon, :listable, :enabled, :badge_grouping_id,
:multiple_grant, :icon, :image, :listable, :enabled, :badge_grouping_id,
:system
has_one :badge_type

View File

@ -53,6 +53,7 @@ class UserSerializer < BasicUserSerializer
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
has_one :card_badge, embed: :object, serializer: BadgeSerializer
staff_attributes :number_of_deleted_posts,
:number_of_flagged_posts,
@ -80,7 +81,9 @@ class UserSerializer < BasicUserSerializer
:disable_jump_reply,
:gravatar_avatar_upload_id,
:custom_avatar_upload_id,
:has_title_badges
:has_title_badges,
:card_image_badge,
:card_image_badge_id
###
### ATTRIBUTES
@ -90,6 +93,11 @@ class UserSerializer < BasicUserSerializer
object.id && object.id == scope.user.try(:id)
end
def card_badge
object.user_profile.card_image_badge
end
def bio_raw
object.user_profile.bio_raw
end
@ -110,6 +118,22 @@ class UserSerializer < BasicUserSerializer
website.present?
end
def card_image_badge_id
object.user_profile.card_image_badge.try(:id)
end
def include_card_image_badge_id?
card_image_badge_id.present?
end
def card_image_badge
object.user_profile.card_image_badge.try(:image)
end
def include_card_image_badge?
card_image_badge.present?
end
def profile_background
object.user_profile.profile_background
end

View File

@ -418,6 +418,9 @@ en:
created: "Joined"
log_out: "Log Out"
location: "Location"
card_badge:
title: "User Card Badge"
website: "Web Site"
email_settings: "Email"
email_digests:
@ -2106,6 +2109,7 @@ en:
listable: Show badge on the public badges page
enabled: Enable badge
icon: Icon
image: Image
icon_help: "Use either a Font Awesome class or URL to an image"
query: Badge Query (SQL)
target_posts: Query targets posts
@ -2206,7 +2210,7 @@ en:
one: "1 granted"
other: "%{count} granted"
select_badge_for_title: Select a badge to use as your title
no_title: "<no title>"
none: "<none>"
badge_grouping:
getting_started:
name: Getting Started

View File

@ -236,6 +236,10 @@ Discourse::Application.routes.draw do
post "users/:username/preferences/user_image" => "users#upload_user_image", constraints: {username: USERNAME_ROUTE_FORMAT}
delete "users/:username/preferences/user_image" => "users#destroy_user_image", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/preferences/card-badge" => "users#card_badge", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/card-badge" => "users#update_card_badge", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/invited" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT}
post "users/action/send_activation_email" => "users#send_activation_email"
get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}

View File

@ -0,0 +1,5 @@
class AddImageToBadges < ActiveRecord::Migration
def change
add_column :badges, :image, :string, limit: 255
end
end

View File

@ -0,0 +1,5 @@
class AddCardImageToUserProfiles < ActiveRecord::Migration
def change
add_column :user_profiles, :card_image_badge_id, :integer
end
end

View File

@ -971,6 +971,26 @@ describe UsersController do
end
end
describe "badge_card" do
let(:user) { Fabricate(:user) }
let(:badge) { Fabricate(:badge) }
let(:user_badge) { BadgeGranter.grant(badge, user) }
it "sets the user's card image to the badge" do
log_in_user user
xhr :put, :update_card_badge, user_badge_id: user_badge.id, username: user.username
user.user_profile.reload.card_image_badge_id.should be_blank
badge.update_attributes image: "wat.com/wat.jpg"
xhr :put, :update_card_badge, user_badge_id: user_badge.id, username: user.username
user.user_profile.reload.card_image_badge_id.should == badge.id
# Can set to nothing
xhr :put, :update_card_badge, username: user.username
user.user_profile.reload.card_image_badge_id.should be_blank
end
end
describe "badge_title" do
let(:user) { Fabricate(:user) }
let(:badge) { Fabricate(:badge) }