mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 17:46:05 -05:00
Work in progress, keeping avatars locally
This introduces a new model to store the avatars and 3 uploads per user (gravatar, system and custom) user can then pick which they want.
This commit is contained in:
parent
4ccf07be8c
commit
6c1c8be794
42 changed files with 626 additions and 319 deletions
|
@ -10,12 +10,14 @@
|
||||||
export default Discourse.Controller.extend(Discourse.ModalFunctionality, {
|
export default Discourse.Controller.extend(Discourse.ModalFunctionality, {
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
useUploadedAvatar: function() { this.set("use_uploaded_avatar", true); },
|
useUploadedAvatar: function() {
|
||||||
useGravatar: function() { this.set("use_uploaded_avatar", false); }
|
this.set("selected", "uploaded");
|
||||||
},
|
},
|
||||||
|
useGravatar: function() {
|
||||||
avatarTemplate: function() {
|
this.set("selected", "gravatar");
|
||||||
return this.get("use_uploaded_avatar") ? this.get("uploaded_avatar_template") : this.get("gravatar_template");
|
},
|
||||||
}.property("use_uploaded_avatar", "uploaded_avatar_template", "gravatar_template")
|
useSystem: function() {
|
||||||
|
this.set("selected", "system");
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -183,16 +183,6 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
var username = Em.get(user, 'username');
|
var username = Em.get(user, 'username');
|
||||||
if (!username) username = Em.get(user, options.hash.usernamePath);
|
if (!username) username = Em.get(user, options.hash.usernamePath);
|
||||||
|
|
||||||
var avatarTemplate;
|
|
||||||
var template = options.hash.template;
|
|
||||||
if (template && template !== 'avatar_template') {
|
|
||||||
avatarTemplate = Em.get(user, template);
|
|
||||||
if (!avatarTemplate) avatarTemplate = Em.get(user, 'user.' + template);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!avatarTemplate) avatarTemplate = Em.get(user, 'avatar_template');
|
|
||||||
if (!avatarTemplate) avatarTemplate = Em.get(user, 'user.avatar_template');
|
|
||||||
|
|
||||||
var title;
|
var title;
|
||||||
if (!options.hash.ignoreTitle) {
|
if (!options.hash.ignoreTitle) {
|
||||||
// first try to get a title
|
// first try to get a title
|
||||||
|
@ -209,6 +199,10 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this is simply done to ensure we cache images correctly
|
||||||
|
var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id') || "_1";
|
||||||
|
var avatarTemplate = Discourse.User.avatarTemplate(username,uploadedAvatarId);
|
||||||
|
|
||||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
||||||
size: options.hash.imageSize,
|
size: options.hash.imageSize,
|
||||||
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
|
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
|
||||||
|
@ -228,11 +222,19 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
@for Handlebars
|
@for Handlebars
|
||||||
**/
|
**/
|
||||||
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
|
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
|
||||||
|
|
||||||
|
var username = Em.get(user, 'username');
|
||||||
|
|
||||||
|
console.log(options.hash);
|
||||||
|
|
||||||
|
var uploadId = (options.hash.uploadId && Em.get(user, options.hash.uploadId)) || Em.get(user, 'uploaded_avatar_id');
|
||||||
|
var avatarTemplate = Discourse.User.avatarTemplate(username,uploadId);
|
||||||
|
|
||||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
||||||
size: options.hash.imageSize,
|
size: options.hash.imageSize,
|
||||||
avatarTemplate: Em.get(user, options.hash.template || 'avatar_template')
|
avatarTemplate: avatarTemplate
|
||||||
}));
|
}));
|
||||||
}, 'avatar_template', 'uploaded_avatar_template', 'gravatar_template');
|
}, 'uploadId', 'username', 'uploaded_avatar_id');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Nicely format a date without binding or returning HTML
|
Nicely format a date without binding or returning HTML
|
||||||
|
|
|
@ -326,6 +326,10 @@ Discourse.User = Discourse.Model.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
avatarTemplate: function(){
|
||||||
|
return Discourse.User.avatarTemplate(this.get('username'),this.get('uploaded_avatar_id'));
|
||||||
|
}.property('uploaded_avatar_id', 'username'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Change avatar selection
|
Change avatar selection
|
||||||
|
|
||||||
|
@ -413,6 +417,11 @@ Discourse.User = Discourse.Model.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
Discourse.User.reopenClass(Discourse.Singleton, {
|
Discourse.User.reopenClass(Discourse.Singleton, {
|
||||||
|
|
||||||
|
avatarTemplate: function(username, uploadedAvatarId){
|
||||||
|
return Discourse.getURL("/avatar/" + username.toLowerCase() + "/{size}/" + uploadedAvatarId + ".png");
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Find a `Discourse.User` for a given username.
|
Find a `Discourse.User` for a given username.
|
||||||
|
|
||||||
|
|
|
@ -184,7 +184,7 @@ Discourse.UserAction = Discourse.Model.extend({
|
||||||
switchToActing: function() {
|
switchToActing: function() {
|
||||||
this.setProperties({
|
this.setProperties({
|
||||||
username: this.get('acting_username'),
|
username: this.get('acting_username'),
|
||||||
avatar_template: this.get('acting_avatar_template'),
|
uploaded_avatar_id: this.get('acting_uploaded_avatar_id'),
|
||||||
name: this.get('actingDisplayName')
|
name: this.get('actingDisplayName')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,11 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
|
||||||
// all the properties needed for displaying the avatar selector modal
|
// all the properties needed for displaying the avatar selector modal
|
||||||
this.controllerFor('avatar-selector').setProperties(this.modelFor('user').getProperties(
|
this.controllerFor('avatar-selector').setProperties(this.modelFor('user').getProperties(
|
||||||
'username', 'email',
|
'username', 'email',
|
||||||
'has_uploaded_avatar', 'use_uploaded_avatar',
|
'system_avatar_upload_id',
|
||||||
'gravatar_template', 'uploaded_avatar_template'));
|
'gravatr_avatar_upload_id',
|
||||||
|
'custom_avatar_upload_id'
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
saveAvatarSelection: function() {
|
saveAvatarSelection: function() {
|
||||||
|
|
|
@ -1,15 +1,19 @@
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="avatar" name="avatar" value="gravatar" {{action useGravatar}}>
|
<input type="radio" id="system-avatar" name="avatar" value="system" {{action useSystem}}>
|
||||||
<label class="radio" for="avatar">{{avatar controller imageSize="large" template="gravatar_template"}} {{{i18n user.change_avatar.gravatar}}} {{email}}</label>
|
<label class="radio" for="system-avatar">{{boundAvatar controller imageSize="large" uploadId="system_avatar_upload_id"}} {{{i18n user.change_avatar.letter_based}}}</label>
|
||||||
<a href="//gravatar.com/emails" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn no-text"><i class="fa fa-pencil"></i></a>
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="radio" id="gravatar" name="avatar" value="gravatar" {{action useGravatar}}>
|
||||||
|
<label class="radio" for="gravatar">{{boundAvatar controller imageSize="large" uploadId="gravatar_avatar_upload_id"}} {{{i18n user.change_avatar.gravatar}}} {{email}}</label>
|
||||||
|
<a href="#" {{action refreshGravatar}} title="{{i18n user.change_avatar.refresh_gravatar_title}}" class="btn no-text"><i class="fa fa-refresh"></i></a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded_avatar" {{action useUploadedAvatar}}>
|
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded_avatar" {{action useUploadedAvatar}}>
|
||||||
<label class="radio" for="uploaded_avatar">
|
<label class="radio" for="uploaded_avatar">
|
||||||
{{#if has_uploaded_avatar}}
|
{{#if custom_avatar_upload_id}}
|
||||||
{{boundAvatar controller imageSize="large" template="uploaded_avatar_template"}} {{i18n user.change_avatar.uploaded_avatar}}
|
{{boundAvatar controller imageSize="large" uploadId="custom_avatar_upload_id"}} {{i18n user.change_avatar.uploaded_avatar}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{i18n user.change_avatar.uploaded_avatar_empty}}
|
{{i18n user.change_avatar.uploaded_avatar_empty}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -105,7 +105,7 @@ Discourse.AvatarSelectorView = Discourse.ModalBodyView.extend({
|
||||||
}.observes('controller.use_uploaded_avatar'),
|
}.observes('controller.use_uploaded_avatar'),
|
||||||
|
|
||||||
uploadButtonText: function() {
|
uploadButtonText: function() {
|
||||||
return this.get("uploading") ? I18n.t("uploading") : I18n.t("upload");
|
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
|
||||||
}.property("uploading")
|
}.property("uploading")
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -182,3 +182,11 @@ animation: modal .25s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-options {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.uploaded-avatar {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
46
app/controllers/avatar_controller.rb
Normal file
46
app/controllers/avatar_controller.rb
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
require_dependency 'letter_avatar'
|
||||||
|
class AvatarController < ApplicationController
|
||||||
|
|
||||||
|
skip_before_filter :check_xhr, :verify_authenticity_token
|
||||||
|
|
||||||
|
def show
|
||||||
|
username = params[:username].to_s
|
||||||
|
raise Discourse::NotFound unless user = User.find_by(username_lower: username.downcase)
|
||||||
|
|
||||||
|
size = params[:size].to_i
|
||||||
|
if size > 1000 || size < 1
|
||||||
|
raise Discourse::NotFound
|
||||||
|
end
|
||||||
|
|
||||||
|
image = nil
|
||||||
|
version = params[:version].to_i
|
||||||
|
|
||||||
|
raise Discourse::NotFound unless version > 0 && user_avatar = user.user_avatar
|
||||||
|
|
||||||
|
upload = version if user_avatar.contains_upload?(version)
|
||||||
|
upload ||= user.uploaded_avatar if user.uploaded_avatar_id == version
|
||||||
|
|
||||||
|
if user.uploaded_avatar && !upload
|
||||||
|
return redirect_to "/avatar/#{user.username_lower}/#{size}/#{user.uploaded_avatar_id}.png"
|
||||||
|
elsif upload
|
||||||
|
# TODO broken with S3 (should retrun a permanent redirect)
|
||||||
|
original = Discourse.store.path_for(user.uploaded_avatar)
|
||||||
|
if File.exists?(original)
|
||||||
|
optimized = OptimizedImage.create_for(
|
||||||
|
user.uploaded_avatar,
|
||||||
|
size,
|
||||||
|
size,
|
||||||
|
allow_animation: SiteSetting.allow_animated_avatars
|
||||||
|
)
|
||||||
|
image = Discourse.store.path_for(optimized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if image
|
||||||
|
expires_in 1.year, public: true
|
||||||
|
send_file image, disposition: nil
|
||||||
|
else
|
||||||
|
raise Discourse::NotFound
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -422,8 +422,6 @@ class UsersController < ApplicationController
|
||||||
|
|
||||||
def upload_avatar_for(user, upload)
|
def upload_avatar_for(user, upload)
|
||||||
user.upload_avatar(upload)
|
user.upload_avatar(upload)
|
||||||
Jobs.enqueue(:generate_avatars, user_id: user.id, upload_id: upload.id)
|
|
||||||
|
|
||||||
render json: { url: upload.url, width: upload.width, height: upload.height }
|
render json: { url: upload.url, width: upload.width, height: upload.height }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
10
app/jobs/regular/create_missing_avatars.rb
Normal file
10
app/jobs/regular/create_missing_avatars.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
module Jobs
|
||||||
|
class CreateMissingAvatars < Jobs::Base
|
||||||
|
def execute(args)
|
||||||
|
User.find_each do |u|
|
||||||
|
u.refresh_avatar
|
||||||
|
u.save
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,67 +0,0 @@
|
||||||
require "image_sorcery"
|
|
||||||
|
|
||||||
module Jobs
|
|
||||||
|
|
||||||
class GenerateAvatars < Jobs::Base
|
|
||||||
|
|
||||||
def execute(args)
|
|
||||||
raise Discourse::ImageMagickMissing.new unless system("command -v convert >/dev/null;")
|
|
||||||
|
|
||||||
upload_id, user_id = args[:upload_id], args[:user_id]
|
|
||||||
raise Discourse::InvalidParameters.new(:upload_id) if upload_id.blank?
|
|
||||||
raise Discourse::InvalidParameters.new(:user_id) if user_id.blank?
|
|
||||||
|
|
||||||
upload = Upload.find_by(id: upload_id)
|
|
||||||
user = User.find_by(id: user_id)
|
|
||||||
return if upload.nil? || user.nil?
|
|
||||||
|
|
||||||
external_copy = Discourse.store.download(upload) if Discourse.store.external?
|
|
||||||
original_path = if Discourse.store.external?
|
|
||||||
external_copy.path
|
|
||||||
else
|
|
||||||
Discourse.store.path_for(upload)
|
|
||||||
end
|
|
||||||
|
|
||||||
source = original_path
|
|
||||||
# extract the first frame when it's a gif
|
|
||||||
source << "[0]" unless SiteSetting.allow_animated_avatars
|
|
||||||
image = ImageSorcery.new(source)
|
|
||||||
extension = File.extname(original_path)
|
|
||||||
|
|
||||||
[120, 45, 32, 25, 20].each do |s|
|
|
||||||
# handle retina too
|
|
||||||
[s, s * 2].each do |size|
|
|
||||||
begin
|
|
||||||
# create a temp file with the same extension as the original
|
|
||||||
temp_file = Tempfile.new(["discourse-avatar", extension])
|
|
||||||
# create a transparent centered square thumbnail
|
|
||||||
if image.convert(temp_file.path,
|
|
||||||
gravity: "center",
|
|
||||||
thumbnail: "#{size}x#{size}^",
|
|
||||||
extent: "#{size}x#{size}",
|
|
||||||
background: "transparent")
|
|
||||||
if Discourse.store.store_avatar(temp_file, upload, size).blank?
|
|
||||||
Rails.logger.error("Failed to store avatar #{size} for #{upload.url} from #{source}")
|
|
||||||
end
|
|
||||||
else
|
|
||||||
Rails.logger.error("Failed to create avatar #{size} for #{upload.url} from #{source}")
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
# close && remove temp file
|
|
||||||
temp_file && temp_file.close!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# make sure we remove the cached copy from external stores
|
|
||||||
external_copy.close! if Discourse.store.external?
|
|
||||||
|
|
||||||
# attach the avatar to the user
|
|
||||||
user.uploaded_avatar_template = Discourse.store.avatar_template(upload)
|
|
||||||
user.save!
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -1,31 +0,0 @@
|
||||||
require_dependency 'avatar_detector'
|
|
||||||
|
|
||||||
module Jobs
|
|
||||||
|
|
||||||
class DetectAvatars < Jobs::Scheduled
|
|
||||||
every 8.hours
|
|
||||||
|
|
||||||
def execute(args)
|
|
||||||
return unless SiteSetting.detect_custom_avatars?
|
|
||||||
|
|
||||||
# Find a random sampling of users of trust level 1 or higher who don't have a custom avatar.
|
|
||||||
user_stats = UserStat.where('user_stats.has_custom_avatar = false AND users.trust_level > 0')
|
|
||||||
.references(:user)
|
|
||||||
.includes(:user)
|
|
||||||
.order("random()")
|
|
||||||
.limit(SiteSetting.max_daily_gravatar_crawls)
|
|
||||||
|
|
||||||
if user_stats.present?
|
|
||||||
user_stats.each do |us|
|
|
||||||
us.update_column(:has_custom_avatar, true) if AvatarDetector.new(us.user).has_custom_avatar?
|
|
||||||
UserHistory.create!(
|
|
||||||
action: UserHistory.actions[:checked_for_custom_avatar],
|
|
||||||
target_user_id: us.user_id
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -33,6 +33,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :topic_links, dependent: :destroy
|
has_many :topic_links, dependent: :destroy
|
||||||
has_many :uploads
|
has_many :uploads
|
||||||
|
|
||||||
|
has_one :user_avatar, dependent: :destroy
|
||||||
has_one :facebook_user_info, dependent: :destroy
|
has_one :facebook_user_info, dependent: :destroy
|
||||||
has_one :twitter_user_info, dependent: :destroy
|
has_one :twitter_user_info, dependent: :destroy
|
||||||
has_one :github_user_info, dependent: :destroy
|
has_one :github_user_info, dependent: :destroy
|
||||||
|
@ -49,7 +50,7 @@ class User < ActiveRecord::Base
|
||||||
has_one :user_search_data, dependent: :destroy
|
has_one :user_search_data, dependent: :destroy
|
||||||
has_one :api_key, dependent: :destroy
|
has_one :api_key, dependent: :destroy
|
||||||
|
|
||||||
belongs_to :uploaded_avatar, class_name: 'Upload', dependent: :destroy
|
belongs_to :uploaded_avatar, class_name: 'Upload'
|
||||||
|
|
||||||
delegate :last_sent_email_address, :to => :email_logs
|
delegate :last_sent_email_address, :to => :email_logs
|
||||||
|
|
||||||
|
@ -72,6 +73,7 @@ class User < ActiveRecord::Base
|
||||||
|
|
||||||
after_create :create_email_token
|
after_create :create_email_token
|
||||||
after_create :create_user_stat
|
after_create :create_user_stat
|
||||||
|
after_save :refresh_avatar
|
||||||
|
|
||||||
before_destroy do
|
before_destroy do
|
||||||
# These tables don't have primary keys, so destroying them with activerecord is tricky:
|
# These tables don't have primary keys, so destroying them with activerecord is tricky:
|
||||||
|
@ -336,25 +338,27 @@ class User < ActiveRecord::Base
|
||||||
"//www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon"
|
"//www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Don't pass this up to the client - it's meant for server side use
|
# Don't pass this up to the client - it's meant for server side use
|
||||||
# This is used in
|
# This is used in
|
||||||
# - self oneboxes in open graph data
|
# - self oneboxes in open graph data
|
||||||
# - emails
|
# - emails
|
||||||
def small_avatar_url
|
def small_avatar_url
|
||||||
template = avatar_template
|
avatar_template_url.gsub("{size}", "45")
|
||||||
schemaless template.gsub("{size}", "45")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# the avatars might take a while to generate
|
def avatar_template_url
|
||||||
# so return the url of the original image in the meantime
|
|
||||||
def uploaded_avatar_path
|
|
||||||
return unless SiteSetting.allow_uploaded_avatars? && use_uploaded_avatar
|
|
||||||
avatar_template = uploaded_avatar_template.present? ? uploaded_avatar_template : uploaded_avatar.try(:url)
|
|
||||||
schemaless absolute avatar_template
|
schemaless absolute avatar_template
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.avatar_template(username,uploaded_avatar_id)
|
||||||
|
id = uploaded_avatar_id || -1
|
||||||
|
username ||= ""
|
||||||
|
"/avatar/#{username.downcase}/{size}/#{id}.png"
|
||||||
|
end
|
||||||
|
|
||||||
def avatar_template
|
def avatar_template
|
||||||
uploaded_avatar_path || User.gravatar_template(id != -1 ? email : "team@discourse.org")
|
self.class.avatar_template(username,uploaded_avatar_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The following count methods are somewhat slow - definitely don't use them in a loop.
|
# The following count methods are somewhat slow - definitely don't use them in a loop.
|
||||||
|
@ -616,6 +620,20 @@ class User < ActiveRecord::Base
|
||||||
Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.id, redirected_at: Time.zone.now)
|
Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.id, redirected_at: Time.zone.now)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def refresh_avatar
|
||||||
|
avatar = user_avatar || UserAvatar.create!(user_id: id)
|
||||||
|
|
||||||
|
if SiteSetting.automatically_download_gravatars?
|
||||||
|
avatar.update_gravatar! unless avatar.last_gravatar_download_attempt
|
||||||
|
end
|
||||||
|
|
||||||
|
if SiteSetting.enable_system_avatars?
|
||||||
|
avatar.update_system_avatar! if !avatar.system_upload_id || username_changed?
|
||||||
|
end
|
||||||
|
|
||||||
|
self.uploaded_avatar_id = (avatar.gravatar_upload_id || avatar.system_upload_id) unless uploaded_avatar_id
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def cook
|
def cook
|
||||||
|
@ -778,7 +796,6 @@ end
|
||||||
# primary_group_id :integer
|
# primary_group_id :integer
|
||||||
# locale :string(10)
|
# locale :string(10)
|
||||||
# profile_background :string(255)
|
# profile_background :string(255)
|
||||||
# email_hash :string(255)
|
|
||||||
# registration_ip_address :inet
|
# registration_ip_address :inet
|
||||||
# last_redirected_to_top_at :datetime
|
# last_redirected_to_top_at :datetime
|
||||||
#
|
#
|
||||||
|
|
|
@ -109,9 +109,9 @@ SELECT
|
||||||
coalesce(p.post_number, 1) post_number,
|
coalesce(p.post_number, 1) post_number,
|
||||||
p.reply_to_post_number,
|
p.reply_to_post_number,
|
||||||
pu.email, pu.username, pu.name, pu.id user_id,
|
pu.email, pu.username, pu.name, pu.id user_id,
|
||||||
pu.use_uploaded_avatar, pu.uploaded_avatar_template, pu.uploaded_avatar_id,
|
pu.uploaded_avatar_id,
|
||||||
u.email acting_email, u.username acting_username, u.name acting_name, u.id acting_user_id,
|
u.email acting_email, u.username acting_username, u.name acting_name, u.id acting_user_id,
|
||||||
u.use_uploaded_avatar acting_use_uploaded_avatar, u.uploaded_avatar_template acting_uploaded_avatar_template, u.uploaded_avatar_id acting_uploaded_avatar_id,
|
u.uploaded_avatar_id acting_uploaded_avatar_id,
|
||||||
coalesce(p.cooked, p2.cooked) cooked,
|
coalesce(p.cooked, p2.cooked) cooked,
|
||||||
CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted,
|
CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted,
|
||||||
p.hidden,
|
p.hidden,
|
||||||
|
|
53
app/models/user_avatar.rb
Normal file
53
app/models/user_avatar.rb
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
require_dependency 'letter_avatar'
|
||||||
|
|
||||||
|
class UserAvatar < ActiveRecord::Base
|
||||||
|
MAX_SIZE = 240
|
||||||
|
|
||||||
|
belongs_to :user
|
||||||
|
belongs_to :system_upload, class_name: 'Upload', dependent: :destroy
|
||||||
|
belongs_to :gravatar_upload, class_name: 'Upload', dependent: :destroy
|
||||||
|
belongs_to :custom_upload, class_name: 'Upload', dependent: :destroy
|
||||||
|
|
||||||
|
def contains_upload?(id)
|
||||||
|
system_upload_id == id || gravatar_upload_id == id || custom_upload_id == id
|
||||||
|
end
|
||||||
|
|
||||||
|
# updates the letter based avatar
|
||||||
|
def update_system_avatar!
|
||||||
|
system_upload.destroy! if system_upload
|
||||||
|
file = File.open(LetterAvatar.generate(user.username, MAX_SIZE), "r")
|
||||||
|
self.system_upload = Upload.create_for(user_id, file, "avatar.png", file.size)
|
||||||
|
save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_gravatar!
|
||||||
|
self.last_gravatar_download_attempt = Time.new
|
||||||
|
gravatar_url = "http://www.gravatar.com/avatar/#{user.email_hash}.png?s=500&d=404"
|
||||||
|
tempfile = FileHelper.download(gravatar_url, 1.megabyte, "gravatar")
|
||||||
|
|
||||||
|
upload = Upload.create_for(user.id, tempfile, 'gravatar.png', File.size(tempfile.path))
|
||||||
|
|
||||||
|
gravatar_upload.destroy! if gravatar_upload
|
||||||
|
self.gravatar_upload = upload
|
||||||
|
save!
|
||||||
|
rescue OpenURI::HTTPError
|
||||||
|
save!
|
||||||
|
ensure
|
||||||
|
tempfile.unlink if tempfile
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# == Schema Information
|
||||||
|
#
|
||||||
|
# Table name: user_avatars
|
||||||
|
#
|
||||||
|
# id :integer not null, primary key
|
||||||
|
# user_id :integer not null
|
||||||
|
# system_upload_id :integer
|
||||||
|
# custom_upload_id :integer
|
||||||
|
# gravatar_upload_id :integer
|
||||||
|
# last_gravatar_download_attempt :datetime
|
||||||
|
# created_at :datetime
|
||||||
|
# updated_at :datetime
|
||||||
|
#
|
|
@ -85,7 +85,6 @@ end
|
||||||
# Table name: user_stats
|
# Table name: user_stats
|
||||||
#
|
#
|
||||||
# user_id :integer not null, primary key
|
# user_id :integer not null, primary key
|
||||||
# has_custom_avatar :boolean default(FALSE), not null
|
|
||||||
# topics_entered :integer default(0), not null
|
# topics_entered :integer default(0), not null
|
||||||
# time_read :integer default(0), not null
|
# time_read :integer default(0), not null
|
||||||
# days_visited :integer default(0), not null
|
# days_visited :integer default(0), not null
|
||||||
|
|
|
@ -4,6 +4,7 @@ class BasicPostSerializer < ApplicationSerializer
|
||||||
:name,
|
:name,
|
||||||
:username,
|
:username,
|
||||||
:avatar_template,
|
:avatar_template,
|
||||||
|
:uploaded_avatar_id,
|
||||||
:created_at,
|
:created_at,
|
||||||
:cooked
|
:cooked
|
||||||
|
|
||||||
|
@ -19,6 +20,10 @@ class BasicPostSerializer < ApplicationSerializer
|
||||||
object.user.try(:avatar_template)
|
object.user.try(:avatar_template)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def uploaded_avatar_id
|
||||||
|
object.user.try(:uploaded_avatar_id)
|
||||||
|
end
|
||||||
|
|
||||||
def cooked
|
def cooked
|
||||||
if object.hidden && !scope.is_staff?
|
if object.hidden && !scope.is_staff?
|
||||||
if scope.current_user && object.user_id == scope.current_user.id
|
if scope.current_user && object.user_id == scope.current_user.id
|
||||||
|
|
|
@ -1,7 +1,26 @@
|
||||||
class BasicUserSerializer < ApplicationSerializer
|
class BasicUserSerializer < ApplicationSerializer
|
||||||
attributes :id, :username, :avatar_template
|
attributes :id, :username, :uploaded_avatar_id, :avatar_template
|
||||||
|
|
||||||
def include_name?
|
def include_name?
|
||||||
SiteSetting.enable_names?
|
SiteSetting.enable_names?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# so weird we send a hash in here sometimes and an object others
|
||||||
|
def include_uploaded_avatar_id?
|
||||||
|
SiteSetting.allow_uploaded_avatars? &&
|
||||||
|
(Hash === object ? user[:uploaded_avatar_id] : object.uploaded_avatar_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def avatar_template
|
||||||
|
if Hash === object
|
||||||
|
User.avatar_template(user[:username], user[:uploaded_avatar_id])
|
||||||
|
else
|
||||||
|
object.avatar_template
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def user
|
||||||
|
object[:user] || object
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -127,7 +127,8 @@ class PostSerializer < BasicPostSerializer
|
||||||
def reply_to_user
|
def reply_to_user
|
||||||
{
|
{
|
||||||
username: object.reply_to_user.username,
|
username: object.reply_to_user.username,
|
||||||
avatar_template: object.reply_to_user.avatar_template
|
avatar_template: object.reply_to_user.avatar_template,
|
||||||
|
uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,15 @@ class TopicPostCountSerializer < BasicUserSerializer
|
||||||
object[:user].username
|
object[:user].username
|
||||||
end
|
end
|
||||||
|
|
||||||
def avatar_template
|
|
||||||
object[:user].avatar_template
|
|
||||||
end
|
|
||||||
|
|
||||||
def post_count
|
def post_count
|
||||||
object[:post_count]
|
object[:post_count]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def uploaded_avatar_id
|
||||||
|
object[:user].uploaded_avatar_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_uploaded_avatar_id?
|
||||||
|
SiteSetting.allow_uploaded_avatars? && object[:user].use_uploaded_avatar
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,30 +23,20 @@ class UserActionSerializer < ApplicationSerializer
|
||||||
:hidden,
|
:hidden,
|
||||||
:moderator_action,
|
:moderator_action,
|
||||||
:edit_reason,
|
:edit_reason,
|
||||||
:category_id
|
:category_id,
|
||||||
|
:uploaded_avatar_id,
|
||||||
|
:acting_uploaded_avatar_id
|
||||||
|
|
||||||
def excerpt
|
def excerpt
|
||||||
PrettyText.excerpt(object.cooked, 300) if object.cooked
|
PrettyText.excerpt(object.cooked, 300) if object.cooked
|
||||||
end
|
end
|
||||||
|
|
||||||
def avatar_template
|
def avatar_template
|
||||||
avatar_for(
|
User.avatar_template(object.username, object.uploaded_avatar_id)
|
||||||
object.user_id,
|
|
||||||
object.email,
|
|
||||||
object.use_uploaded_avatar,
|
|
||||||
object.uploaded_avatar_template,
|
|
||||||
object.uploaded_avatar_id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def acting_avatar_template
|
def acting_avatar_template
|
||||||
avatar_for(
|
User.avatar_template(object.acting_username, object.acting_uploaded_avatar_id)
|
||||||
object.acting_user_id,
|
|
||||||
object.acting_email,
|
|
||||||
object.acting_use_uploaded_avatar,
|
|
||||||
object.acting_uploaded_avatar_template,
|
|
||||||
object.acting_uploaded_avatar_id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_name?
|
def include_name?
|
||||||
|
|
|
@ -63,16 +63,27 @@ class UserSerializer < BasicUserSerializer
|
||||||
:external_links_in_new_tab,
|
:external_links_in_new_tab,
|
||||||
:dynamic_favicon,
|
:dynamic_favicon,
|
||||||
:enable_quoting,
|
:enable_quoting,
|
||||||
:use_uploaded_avatar,
|
|
||||||
:has_uploaded_avatar,
|
|
||||||
:gravatar_template,
|
|
||||||
:uploaded_avatar_template,
|
|
||||||
:muted_category_ids,
|
:muted_category_ids,
|
||||||
:tracked_category_ids,
|
:tracked_category_ids,
|
||||||
:watched_category_ids,
|
:watched_category_ids,
|
||||||
:private_messages_stats,
|
:private_messages_stats,
|
||||||
:disable_jump_reply
|
:disable_jump_reply,
|
||||||
|
:system_avatar_upload_id,
|
||||||
|
:gravatar_avatar_upload_id,
|
||||||
|
:custom_avatar_upload_id,
|
||||||
|
:uploaded_avatar_id
|
||||||
|
|
||||||
|
def system_avatar_upload_id
|
||||||
|
object.user_avatar.try(:system_upload_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def gravatar_avatar_upload_id
|
||||||
|
object.user_avatar.try(:gravatar_upload_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def custom_avatar_upload_id
|
||||||
|
object.user_avatar.try(:custom_upload_id)
|
||||||
|
end
|
||||||
|
|
||||||
def auto_track_topics_after_msecs
|
def auto_track_topics_after_msecs
|
||||||
object.auto_track_topics_after_msecs || SiteSetting.auto_track_topics_after
|
object.auto_track_topics_after_msecs || SiteSetting.auto_track_topics_after
|
||||||
|
@ -106,10 +117,6 @@ class UserSerializer < BasicUserSerializer
|
||||||
UserAction.stats(object.id, scope)
|
UserAction.stats(object.id, scope)
|
||||||
end
|
end
|
||||||
|
|
||||||
def gravatar_template
|
|
||||||
User.gravatar_template(object.email)
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_suspended?
|
def include_suspended?
|
||||||
object.suspended?
|
object.suspended?
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,6 +22,7 @@ if defined?(Rack::MiniProfiler)
|
||||||
(path !~ /^\/message-bus/) &&
|
(path !~ /^\/message-bus/) &&
|
||||||
(path !~ /topics\/timings/) &&
|
(path !~ /topics\/timings/) &&
|
||||||
(path !~ /assets/) &&
|
(path !~ /assets/) &&
|
||||||
|
(path !~ /\/avatar\//) &&
|
||||||
(path !~ /qunit/) &&
|
(path !~ /qunit/) &&
|
||||||
(path !~ /srv\/status/) &&
|
(path !~ /srv\/status/) &&
|
||||||
(path !~ /commits-widget/) &&
|
(path !~ /commits-widget/) &&
|
||||||
|
|
|
@ -307,10 +307,12 @@ en:
|
||||||
change_avatar:
|
change_avatar:
|
||||||
title: "Change your avatar"
|
title: "Change your avatar"
|
||||||
gravatar: "<a href='//gravatar.com/emails' target='_blank'>Gravatar</a>, based on"
|
gravatar: "<a href='//gravatar.com/emails' target='_blank'>Gravatar</a>, based on"
|
||||||
gravatar_title: "Change your avatar on Gravatar's website"
|
refresh_gravatar_title: "Refresh your Gravatar"
|
||||||
|
letter_based: "System assigned avatar"
|
||||||
uploaded_avatar: "Custom picture"
|
uploaded_avatar: "Custom picture"
|
||||||
uploaded_avatar_empty: "Add a custom picture"
|
uploaded_avatar_empty: "Add a custom picture"
|
||||||
upload_title: "Upload your picture"
|
upload_title: "Upload your picture"
|
||||||
|
upload_picture: "Upload Picture"
|
||||||
image_is_not_a_square: "Warning: we've cropped your image; it is not square."
|
image_is_not_a_square: "Warning: we've cropped your image; it is not square."
|
||||||
|
|
||||||
change_profile_background:
|
change_profile_background:
|
||||||
|
|
|
@ -863,6 +863,7 @@ en:
|
||||||
|
|
||||||
allow_uploaded_avatars: "Allow users to upload their custom avatars"
|
allow_uploaded_avatars: "Allow users to upload their custom avatars"
|
||||||
allow_animated_avatars: "Allow users to use animated gif for avatars. WARNING: it is highly recommended to run the avatars:regenerate rake task after changing that setting."
|
allow_animated_avatars: "Allow users to use animated gif for avatars. WARNING: it is highly recommended to run the avatars:regenerate rake task after changing that setting."
|
||||||
|
automatically_download_gravatars: "Download gravatars for users upon account creation or email change"
|
||||||
digest_topics: "The maximum amount of topics to display in an email digest"
|
digest_topics: "The maximum amount of topics to display in an email digest"
|
||||||
digest_min_excerpt_length: "How many characters we're aiming for for each post in the email digest"
|
digest_min_excerpt_length: "How many characters we're aiming for for each post in the email digest"
|
||||||
default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences."
|
default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences."
|
||||||
|
|
|
@ -366,6 +366,8 @@ Discourse::Application.routes.draw do
|
||||||
post "draft" => "draft#update"
|
post "draft" => "draft#update"
|
||||||
delete "draft" => "draft#destroy"
|
delete "draft" => "draft#destroy"
|
||||||
|
|
||||||
|
get "avatar/:username/:size/:version.png" => "avatar#show", format: false
|
||||||
|
|
||||||
get "cdn_asset/:site/*path" => "static#cdn_asset", format: false
|
get "cdn_asset/:site/*path" => "static#cdn_asset", format: false
|
||||||
|
|
||||||
get "robots.txt" => "robots_txt#index"
|
get "robots.txt" => "robots_txt#index"
|
||||||
|
|
|
@ -329,14 +329,13 @@ files:
|
||||||
allow_profile_backgrounds:
|
allow_profile_backgrounds:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
|
automatically_download_gravatars: true
|
||||||
allow_uploaded_avatars:
|
allow_uploaded_avatars:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
allow_animated_avatars:
|
allow_animated_avatars:
|
||||||
client: true
|
client: true
|
||||||
default: false
|
default: false
|
||||||
detect_custom_avatars: true
|
|
||||||
max_daily_gravatar_crawls: 500
|
|
||||||
|
|
||||||
trust:
|
trust:
|
||||||
default_trust_level: 0
|
default_trust_level: 0
|
||||||
|
@ -547,7 +546,11 @@ uncategorized:
|
||||||
default: ''
|
default: ''
|
||||||
|
|
||||||
enable_cdn_js_debugging: true
|
enable_cdn_js_debugging: true
|
||||||
|
|
||||||
show_create_topics_notice:
|
show_create_topics_notice:
|
||||||
client: true
|
client: true
|
||||||
default: true
|
default: true
|
||||||
|
|
||||||
|
enable_system_avatars:
|
||||||
|
hidden: true
|
||||||
|
default: true
|
||||||
|
|
|
@ -23,3 +23,8 @@ User.seed do |u|
|
||||||
u.email_private_messages = false
|
u.email_private_messages = false
|
||||||
u.trust_level = TrustLevel.levels[:elder]
|
u.trust_level = TrustLevel.levels[:elder]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# download avatars for existing users
|
||||||
|
if UserAvatar.count < User.count
|
||||||
|
Jobs.enqueue(:create_missing_avatars)
|
||||||
|
end
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class RemoveHasCustomAvatarFromUserStats < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
remove_column :user_stats, :has_custom_avatar
|
||||||
|
end
|
||||||
|
end
|
12
db/migrate/20140522003151_add_user_avatars.rb
Normal file
12
db/migrate/20140522003151_add_user_avatars.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
class AddUserAvatars < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :user_avatars do |t|
|
||||||
|
t.integer :user_id, null: false
|
||||||
|
t.integer :system_upload_id
|
||||||
|
t.integer :custom_upload_id
|
||||||
|
t.integer :gravatar_upload_id
|
||||||
|
t.datetime :last_gravatar_download_attempt
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,32 +0,0 @@
|
||||||
require_dependency 'user'
|
|
||||||
require 'net/http'
|
|
||||||
|
|
||||||
class AvatarDetector
|
|
||||||
|
|
||||||
def initialize(user)
|
|
||||||
raise "Tried to detect an avatar on a non-user instance" unless user && user.is_a?(User)
|
|
||||||
|
|
||||||
@user = user
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_custom_avatar?
|
|
||||||
return true if @user.uploaded_avatar_path
|
|
||||||
has_custom_gravatar?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Check whether the user has a gravatar by performing a HTTP HEAD request to
|
|
||||||
# Gravatar using the `d=404` parameter.
|
|
||||||
def has_custom_gravatar?
|
|
||||||
result = Net::HTTP.start('www.gravatar.com') do |http|
|
|
||||||
http.open_timeout = 2
|
|
||||||
http.read_timeout = 2
|
|
||||||
http.head("/avatar/#{User.email_hash(@user.email)}?d=404")
|
|
||||||
end
|
|
||||||
|
|
||||||
return result.code.to_i == 200
|
|
||||||
rescue
|
|
||||||
# If the HTTP request fails, assume no gravatar
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -15,8 +15,6 @@ class AvatarLookup
|
||||||
@lookup_columns ||= [:id,
|
@lookup_columns ||= [:id,
|
||||||
:email,
|
:email,
|
||||||
:username,
|
:username,
|
||||||
:use_uploaded_avatar,
|
|
||||||
:uploaded_avatar_template,
|
|
||||||
:uploaded_avatar_id]
|
:uploaded_avatar_id]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ class ComposerMessagesFinder
|
||||||
return unless @user.has_trust_level?(:basic)
|
return unless @user.has_trust_level?(:basic)
|
||||||
|
|
||||||
# We don't notify users who have avatars or who have been notified already.
|
# We don't notify users who have avatars or who have been notified already.
|
||||||
return if @user.user_stat.has_custom_avatar? || UserHistory.exists_for_user?(@user, :notified_about_avatar)
|
return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar)
|
||||||
|
|
||||||
# Finally, we don't check users whose avatars haven't been examined
|
# Finally, we don't check users whose avatars haven't been examined
|
||||||
return unless UserHistory.exists_for_user?(@user, :checked_for_custom_avatar)
|
return unless UserHistory.exists_for_user?(@user, :checked_for_custom_avatar)
|
||||||
|
|
303
lib/letter_avatar.rb
Normal file
303
lib/letter_avatar.rb
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
class LetterAvatar
|
||||||
|
class<<self
|
||||||
|
|
||||||
|
FULLSIZE = 240
|
||||||
|
|
||||||
|
def cache_path
|
||||||
|
'tmp/letter_avatars'
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate(username, size)
|
||||||
|
|
||||||
|
size = FULLSIZE if size > FULLSIZE
|
||||||
|
filename = cached_path(username, size)
|
||||||
|
|
||||||
|
if File.exists?(filename)
|
||||||
|
return filename
|
||||||
|
end
|
||||||
|
|
||||||
|
fullsize = fullsize_path(username)
|
||||||
|
if !File.exists?(fullsize)
|
||||||
|
generate_fullsize(username)
|
||||||
|
end
|
||||||
|
|
||||||
|
resize(fullsize, filename, size)
|
||||||
|
end
|
||||||
|
|
||||||
|
def cached_path(username, size)
|
||||||
|
dir = "#{cache_path}/#{username}"
|
||||||
|
FileUtils.mkdir_p(dir)
|
||||||
|
|
||||||
|
"#{dir}/#{size}.png"
|
||||||
|
end
|
||||||
|
|
||||||
|
def fullsize_path(username)
|
||||||
|
cached_path(username, FULLSIZE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def resize(from, to, size)
|
||||||
|
`convert #{from} -resize #{size}x#{size} #{to}`
|
||||||
|
to
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_fullsize(username)
|
||||||
|
|
||||||
|
filename = fullsize_path(username)
|
||||||
|
color = colors[Digest::MD5.hexdigest(username)[0...15].to_i(16) % 216]
|
||||||
|
stroke = darken(color, 0.8)
|
||||||
|
|
||||||
|
instructions = %W{
|
||||||
|
-size 240x240
|
||||||
|
xc:#{to_rgb(color)}
|
||||||
|
-pointsize 240
|
||||||
|
-fill white
|
||||||
|
-gravity Center
|
||||||
|
-font 'Helvetica-Bold'
|
||||||
|
-stroke #{to_rgb(stroke)}
|
||||||
|
-strokewidth 2
|
||||||
|
-annotate -5+25 '#{username[0].upcase}'
|
||||||
|
'#{filename}'
|
||||||
|
}
|
||||||
|
|
||||||
|
`convert #{instructions.join(" ")}`
|
||||||
|
|
||||||
|
filename
|
||||||
|
end
|
||||||
|
|
||||||
|
def darken(color,pct)
|
||||||
|
color.map do |n|
|
||||||
|
(n.to_f * pct).to_i
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_rgb(color)
|
||||||
|
r,g,b = color
|
||||||
|
"'rgb(#{r},#{g},#{b})'"
|
||||||
|
end
|
||||||
|
|
||||||
|
# palette of 216 optimally disctinct colors
|
||||||
|
# cf. http://tools.medialab.sciences-po.fr/iwanthue/index.php
|
||||||
|
# parameters used:
|
||||||
|
# - H: 0 - 360
|
||||||
|
# - C: 0 - 2
|
||||||
|
# - L: 0.75 - 1.5
|
||||||
|
def colors
|
||||||
|
[[198,125,40],
|
||||||
|
[61,155,243],
|
||||||
|
[74,243,75],
|
||||||
|
[238,89,166],
|
||||||
|
[52,240,224],
|
||||||
|
[177,156,155],
|
||||||
|
[247,242,53],
|
||||||
|
[111,154,78],
|
||||||
|
[237,179,245],
|
||||||
|
[237,101,95],
|
||||||
|
[89,239,155],
|
||||||
|
[242,233,158],
|
||||||
|
[163,212,245],
|
||||||
|
[65,152,142],
|
||||||
|
[165,135,246],
|
||||||
|
[181,166,38],
|
||||||
|
[187,229,206],
|
||||||
|
[77,164,25],
|
||||||
|
[179,246,101],
|
||||||
|
[234,93,37],
|
||||||
|
[225,155,115],
|
||||||
|
[142,140,188],
|
||||||
|
[223,120,140],
|
||||||
|
[249,174,27],
|
||||||
|
[244,117,225],
|
||||||
|
[137,141,102],
|
||||||
|
[75,191,146],
|
||||||
|
[188,239,142],
|
||||||
|
[164,199,145],
|
||||||
|
[173,120,149],
|
||||||
|
[59,195,89],
|
||||||
|
[222,198,220],
|
||||||
|
[68,145,187],
|
||||||
|
[236,204,179],
|
||||||
|
[159,195,72],
|
||||||
|
[188,121,189],
|
||||||
|
[166,160,85],
|
||||||
|
[181,233,37],
|
||||||
|
[236,177,85],
|
||||||
|
[121,147,160],
|
||||||
|
[234,218,110],
|
||||||
|
[241,157,191],
|
||||||
|
[62,200,234],
|
||||||
|
[133,243,34],
|
||||||
|
[88,149,110],
|
||||||
|
[226,235,237],
|
||||||
|
[183,119,118],
|
||||||
|
[192,247,192],
|
||||||
|
[113,196,122],
|
||||||
|
[197,115,70],
|
||||||
|
[80,175,187],
|
||||||
|
[103,231,238],
|
||||||
|
[240,72,133],
|
||||||
|
[228,149,241],
|
||||||
|
[180,188,159],
|
||||||
|
[172,132,85],
|
||||||
|
[221,236,102],
|
||||||
|
[236,194,58],
|
||||||
|
[217,176,109],
|
||||||
|
[88,244,199],
|
||||||
|
[186,157,239],
|
||||||
|
[113,230,96],
|
||||||
|
[206,115,165],
|
||||||
|
[244,178,163],
|
||||||
|
[230,139,26],
|
||||||
|
[241,125,89],
|
||||||
|
[83,160,66],
|
||||||
|
[107,190,166],
|
||||||
|
[197,161,210],
|
||||||
|
[198,203,245],
|
||||||
|
[238,117,19],
|
||||||
|
[228,119,116],
|
||||||
|
[131,156,41],
|
||||||
|
[145,178,168],
|
||||||
|
[139,170,220],
|
||||||
|
[233,95,125],
|
||||||
|
[87,178,230],
|
||||||
|
[157,200,119],
|
||||||
|
[237,140,76],
|
||||||
|
[229,185,186],
|
||||||
|
[144,206,212],
|
||||||
|
[236,209,158],
|
||||||
|
[185,189,79],
|
||||||
|
[34,208,66],
|
||||||
|
[84,238,129],
|
||||||
|
[133,140,134],
|
||||||
|
[219,229,175],
|
||||||
|
[168,179,25],
|
||||||
|
[140,145,240],
|
||||||
|
[151,241,125],
|
||||||
|
[67,162,107],
|
||||||
|
[200,156,21],
|
||||||
|
[169,173,189],
|
||||||
|
[226,116,189],
|
||||||
|
[133,231,191],
|
||||||
|
[194,161,63],
|
||||||
|
[241,77,99],
|
||||||
|
[241,217,53],
|
||||||
|
[123,204,105],
|
||||||
|
[210,201,119],
|
||||||
|
[229,108,155],
|
||||||
|
[240,91,72],
|
||||||
|
[187,115,210],
|
||||||
|
[240,163,100],
|
||||||
|
[178,217,57],
|
||||||
|
[179,135,116],
|
||||||
|
[204,211,24],
|
||||||
|
[186,135,57],
|
||||||
|
[223,176,135],
|
||||||
|
[204,148,151],
|
||||||
|
[116,223,50],
|
||||||
|
[95,195,46],
|
||||||
|
[123,160,236],
|
||||||
|
[181,172,131],
|
||||||
|
[142,220,202],
|
||||||
|
[240,140,112],
|
||||||
|
[172,145,164],
|
||||||
|
[228,124,45],
|
||||||
|
[222,234,142],
|
||||||
|
[42,205,125],
|
||||||
|
[192,233,116],
|
||||||
|
[119,170,114],
|
||||||
|
[158,138,26],
|
||||||
|
[73,190,183],
|
||||||
|
[185,229,243],
|
||||||
|
[227,107,55],
|
||||||
|
[196,205,202],
|
||||||
|
[132,143,60],
|
||||||
|
[233,192,237],
|
||||||
|
[62,150,220],
|
||||||
|
[205,201,141],
|
||||||
|
[106,140,190],
|
||||||
|
[161,131,205],
|
||||||
|
[226,226,75],
|
||||||
|
[198,139,81],
|
||||||
|
[115,171,32],
|
||||||
|
[101,181,67],
|
||||||
|
[149,137,119],
|
||||||
|
[222,245,86],
|
||||||
|
[183,130,175],
|
||||||
|
[168,125,133],
|
||||||
|
[124,142,87],
|
||||||
|
[236,156,171],
|
||||||
|
[232,194,91],
|
||||||
|
[219,200,69],
|
||||||
|
[144,219,34],
|
||||||
|
[219,95,187],
|
||||||
|
[145,154,217],
|
||||||
|
[165,185,100],
|
||||||
|
[127,238,163],
|
||||||
|
[224,178,198],
|
||||||
|
[119,153,120],
|
||||||
|
[124,212,92],
|
||||||
|
[172,161,105],
|
||||||
|
[231,155,135],
|
||||||
|
[157,132,101],
|
||||||
|
[122,185,146],
|
||||||
|
[53,166,51],
|
||||||
|
[70,163,90],
|
||||||
|
[150,190,213],
|
||||||
|
[234,235,219],
|
||||||
|
[166,152,185],
|
||||||
|
[159,194,159],
|
||||||
|
[222,242,47],
|
||||||
|
[202,176,161],
|
||||||
|
[95,140,229],
|
||||||
|
[156,246,80],
|
||||||
|
[93,170,203],
|
||||||
|
[159,142,54],
|
||||||
|
[185,237,230],
|
||||||
|
[94,150,149],
|
||||||
|
[187,206,136],
|
||||||
|
[157,224,166],
|
||||||
|
[235,158,208],
|
||||||
|
[109,232,216],
|
||||||
|
[141,201,87],
|
||||||
|
[208,124,118],
|
||||||
|
[142,125,214],
|
||||||
|
[226,234,123],
|
||||||
|
[72,219,41],
|
||||||
|
[234,102,111],
|
||||||
|
[168,142,79],
|
||||||
|
[188,135,35],
|
||||||
|
[214,233,195],
|
||||||
|
[148,173,116],
|
||||||
|
[223,112,95],
|
||||||
|
[228,128,236],
|
||||||
|
[206,114,54],
|
||||||
|
[195,119,88],
|
||||||
|
[235,140,94],
|
||||||
|
[235,202,125],
|
||||||
|
[233,155,153],
|
||||||
|
[214,214,238],
|
||||||
|
[246,200,35],
|
||||||
|
[151,125,171],
|
||||||
|
[132,145,172],
|
||||||
|
[131,142,118],
|
||||||
|
[199,126,150],
|
||||||
|
[61,162,123],
|
||||||
|
[58,176,151],
|
||||||
|
[215,141,69],
|
||||||
|
[225,154,220],
|
||||||
|
[224,244,174],
|
||||||
|
[233,161,64],
|
||||||
|
[130,221,137],
|
||||||
|
[81,191,129],
|
||||||
|
[169,162,140],
|
||||||
|
[174,177,222],
|
||||||
|
[236,174,47],
|
||||||
|
[196,240,159],
|
||||||
|
[69,222,172],
|
||||||
|
[71,232,93],
|
||||||
|
[118,211,238],
|
||||||
|
[157,224,83],
|
||||||
|
[218,105,73],
|
||||||
|
[165,241,221]]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,12 +0,0 @@
|
||||||
desc "re-generate avatars"
|
|
||||||
task "avatars:regenerate" => :environment do
|
|
||||||
RailsMultisite::ConnectionManagement.each_connection do |db|
|
|
||||||
puts "Generating avatars for: #{db}"
|
|
||||||
next unless SiteSetting.allow_uploaded_avatars
|
|
||||||
User.where("uploaded_avatar_id IS NOT NULL").find_each do |u|
|
|
||||||
Jobs.enqueue(:generate_avatars, upload_id: u.uploaded_avatar_id, user_id: u.id)
|
|
||||||
putc "."
|
|
||||||
end
|
|
||||||
end
|
|
||||||
puts "\ndone."
|
|
||||||
end
|
|
|
@ -1,67 +0,0 @@
|
||||||
# encoding: utf-8
|
|
||||||
require 'spec_helper'
|
|
||||||
require_dependency 'avatar_detector'
|
|
||||||
|
|
||||||
describe AvatarDetector do
|
|
||||||
|
|
||||||
describe "construction" do
|
|
||||||
|
|
||||||
it "raises an error without a user" do
|
|
||||||
-> { AvatarDetector.new(nil) }.should raise_error
|
|
||||||
end
|
|
||||||
|
|
||||||
it "raises an error on a non-user object" do
|
|
||||||
-> { AvatarDetector.new(Array.new) }.should raise_error
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "has_custom_avatar?" do
|
|
||||||
|
|
||||||
describe "with a user" do
|
|
||||||
let(:user) { User.new(use_uploaded_avatar: true) }
|
|
||||||
let(:avatar_detector) { AvatarDetector.new(user) }
|
|
||||||
|
|
||||||
describe "when the user doesn't have an uploaded_avatar_path" do
|
|
||||||
|
|
||||||
before do
|
|
||||||
user.stubs(:uploaded_avatar_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns true if they have a custom gravatar" do
|
|
||||||
avatar_detector.expects(:has_custom_gravatar?).returns(true)
|
|
||||||
avatar_detector.has_custom_avatar?.should be_true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns false if they don't have a custom gravatar" do
|
|
||||||
avatar_detector.expects(:has_custom_gravatar?).returns(false)
|
|
||||||
avatar_detector.has_custom_avatar?.should be_false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
context "when the user doesn't have an uploaded_avatar_path" do
|
|
||||||
let(:user) { User.new(use_uploaded_avatar: true) }
|
|
||||||
let(:avatar_detector) { AvatarDetector.new(user) }
|
|
||||||
|
|
||||||
describe "when the user has an uploaded avatar" do
|
|
||||||
before do
|
|
||||||
user.expects(:uploaded_avatar_path).returns("/some/uploaded/file.png")
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns true" do
|
|
||||||
avatar_detector.has_custom_avatar?.should be_true
|
|
||||||
end
|
|
||||||
|
|
||||||
it "doesn't call has_custom_gravatar" do
|
|
||||||
avatar_detector.expects(:has_custom_gravatar?).never
|
|
||||||
avatar_detector.has_custom_avatar?
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
|
@ -112,7 +112,7 @@ describe ComposerMessagesFinder do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't return notifications for users who have custom avatars" do
|
it "doesn't return notifications for users who have custom avatars" do
|
||||||
user.user_stat.has_custom_avatar = true
|
user.uploaded_avatar_id = 1
|
||||||
finder.check_avatar_notification.should be_blank
|
finder.check_avatar_notification.should be_blank
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1136,7 +1136,6 @@ describe UsersController do
|
||||||
upload = Fabricate(:upload)
|
upload = Fabricate(:upload)
|
||||||
Upload.expects(:create_for).returns(upload)
|
Upload.expects(:create_for).returns(upload)
|
||||||
# enqueues the user_image generator job
|
# enqueues the user_image generator job
|
||||||
Jobs.expects(:enqueue).with(:generate_avatars, { user_id: user.id, upload_id: upload.id })
|
|
||||||
xhr :post, :upload_user_image, username: user.username, file: user_image, user_image_type: "avatar"
|
xhr :post, :upload_user_image, username: user.username, file: user_image, user_image_type: "avatar"
|
||||||
user.reload
|
user.reload
|
||||||
# erase the previous template
|
# erase the previous template
|
||||||
|
@ -1194,7 +1193,6 @@ describe UsersController do
|
||||||
upload = Fabricate(:upload)
|
upload = Fabricate(:upload)
|
||||||
Upload.expects(:create_for).returns(upload)
|
Upload.expects(:create_for).returns(upload)
|
||||||
# enqueues the user_image generator job
|
# enqueues the user_image generator job
|
||||||
Jobs.expects(:enqueue).with(:generate_avatars, { user_id: user.id, upload_id: upload.id })
|
|
||||||
xhr :post, :upload_avatar, username: user.username, file: user_image_url, user_image_type: "avatar"
|
xhr :post, :upload_avatar, username: user.username, file: user_image_url, user_image_type: "avatar"
|
||||||
user.reload
|
user.reload
|
||||||
# erase the previous template
|
# erase the previous template
|
||||||
|
|
21
spec/models/user_avatar_spec.rb
Normal file
21
spec/models/user_avatar_spec.rb
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe UserAvatar do
|
||||||
|
let(:avatar){
|
||||||
|
user = Fabricate(:user)
|
||||||
|
user.create_user_avatar!
|
||||||
|
}
|
||||||
|
|
||||||
|
it 'can generate a system avatar' do
|
||||||
|
avatar.update_system_avatar!
|
||||||
|
avatar.system_upload.should_not be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can update gravatars' do
|
||||||
|
temp = Tempfile.new('test')
|
||||||
|
FileHelper.expects(:download).returns(temp)
|
||||||
|
avatar.update_gravatar!
|
||||||
|
temp.unlink
|
||||||
|
avatar.gravatar_upload.should_not be_nil
|
||||||
|
end
|
||||||
|
end
|
|
@ -984,47 +984,30 @@ describe User do
|
||||||
|
|
||||||
describe ".small_avatar_url" do
|
describe ".small_avatar_url" do
|
||||||
|
|
||||||
let(:user) { build(:user, use_uploaded_avatar: true, uploaded_avatar_template: "/uploaded/avatar/template/{size}.png") }
|
let(:user) { build(:user, username: 'Sam') }
|
||||||
|
|
||||||
it "returns a 45-pixel-wide avatar" do
|
it "returns a 45-pixel-wide avatar" do
|
||||||
user.small_avatar_url.should == "//test.localhost/uploaded/avatar/template/45.png"
|
user.small_avatar_url.should == "//test.localhost/avatar/sam/45/-1.png"
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
describe ".uploaded_avatar_path" do
|
describe ".avatar_template_url" do
|
||||||
|
|
||||||
let(:user) { build(:user, use_uploaded_avatar: true, uploaded_avatar_template: "/uploaded/avatar/template/{size}.png") }
|
let(:user) { build(:user, uploaded_avatar_id: 99, username: 'Sam') }
|
||||||
|
|
||||||
it "returns nothing when uploaded avatars are not allowed" do
|
it "returns default when uploaded avatars are not allowed" do
|
||||||
SiteSetting.expects(:allow_uploaded_avatars).returns(false)
|
SiteSetting.allow_uploaded_avatars = false
|
||||||
user.uploaded_avatar_path.should be_nil
|
user.avatar_template_url.should == "//test.localhost/avatar/sam/{size}/-1.png"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a schemaless avatar template" do
|
it "returns a schemaless avatar template with correct id" do
|
||||||
user.uploaded_avatar_path.should == "//test.localhost/uploaded/avatar/template/{size}.png"
|
user.avatar_template_url.should == "//test.localhost/avatar/sam/{size}/99.png"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns a schemaless cdn-based avatar template" do
|
it "returns a schemaless cdn-based avatar template" do
|
||||||
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
|
Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com")
|
||||||
user.uploaded_avatar_path.should == "//my.cdn.com/uploaded/avatar/template/{size}.png"
|
user.avatar_template_url.should == "//my.cdn.com/avatar/sam/{size}/99.png"
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
describe ".avatar_template" do
|
|
||||||
|
|
||||||
let(:user) { build(:user, email: "em@il.com") }
|
|
||||||
|
|
||||||
it "returns the uploaded_avatar_path by default" do
|
|
||||||
user.expects(:uploaded_avatar_path).returns("//discourse.org/uploaded/avatar.png")
|
|
||||||
user.avatar_template.should == "//discourse.org/uploaded/avatar.png"
|
|
||||||
end
|
|
||||||
|
|
||||||
it "returns the gravatar when no avatar has been uploaded" do
|
|
||||||
user.expects(:uploaded_avatar_path)
|
|
||||||
User.expects(:gravatar_template).with(user.email).returns("//gravatar.com/avatar.png")
|
|
||||||
user.avatar_template.should == "//gravatar.com/avatar.png"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,6 +33,9 @@ Spork.prefork do
|
||||||
|
|
||||||
# let's not run seed_fu every test
|
# let's not run seed_fu every test
|
||||||
SeedFu.quiet = true if SeedFu.respond_to? :quiet
|
SeedFu.quiet = true if SeedFu.respond_to? :quiet
|
||||||
|
|
||||||
|
SiteSetting.enable_system_avatars = false
|
||||||
|
SiteSetting.automatically_download_gravatars = false
|
||||||
SeedFu.seed
|
SeedFu.seed
|
||||||
|
|
||||||
RSpec.configure do |config|
|
RSpec.configure do |config|
|
||||||
|
@ -79,6 +82,10 @@ Spork.prefork do
|
||||||
SiteSetting.remove_override!(setting.name)
|
SiteSetting.remove_override!(setting.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# very expensive IO operations
|
||||||
|
SiteSetting.enable_system_avatars = false
|
||||||
|
SiteSetting.automatically_download_gravatars = false
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class TestCurrentUserProvider < Auth::DefaultCurrentUserProvider
|
class TestCurrentUserProvider < Auth::DefaultCurrentUserProvider
|
||||||
|
|
Loading…
Reference in a new issue