diff --git a/app/assets/javascripts/discourse/components/bbcode.js b/app/assets/javascripts/discourse/components/bbcode.js
index 50cd2fdbd6..27c8372fa4 100644
--- a/app/assets/javascripts/discourse/components/bbcode.js
+++ b/app/assets/javascripts/discourse/components/bbcode.js
@@ -252,12 +252,22 @@ Discourse.BBCode = {
// remove leading s
var content = matches[2].trim();
+ var avatarImg;
+ if (opts.lookupAvatarByPostNumber) {
+ // client-side, we can retrieve the avatar from the post
+ var postNumber = parseInt(_.find(params, { 'key' : 'post' }).value, 10);
+ avatarImg = opts.lookupAvatarByPostNumber(postNumber);
+ } else if (opts.lookupAvatar) {
+ // server-side, we need to lookup the avatar from the username
+ avatarImg = opts.lookupAvatar(username);
+ }
+
// Arguments for formatting
args = {
- username: I18n.t('user.said',{username: username}),
+ username: I18n.t('user.said', {username: username}),
params: params,
quote: content,
- avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0
+ avatarImg: avatarImg
};
// Name of the template
diff --git a/app/assets/javascripts/discourse/components/utilities.js b/app/assets/javascripts/discourse/components/utilities.js
index 454e0c6183..bf2b0f0ec3 100644
--- a/app/assets/javascripts/discourse/components/utilities.js
+++ b/app/assets/javascripts/discourse/components/utilities.js
@@ -16,6 +16,7 @@ Discourse.Utilities = {
case 'small': return 25;
case 'medium': return 32;
case 'large': return 45;
+ case 'huge': return 120;
}
return size;
},
@@ -50,18 +51,20 @@ Discourse.Utilities = {
return result + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "";
},
- avatarUrl: function(username, size, template) {
- if (!username) return "";
- var rawSize = (Discourse.Utilities.translateSize(size) * (window.devicePixelRatio || 1)).toFixed();
+ avatarUrl: function(template, size) {
+ if (!template) { return ""; }
+ var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size));
+ return template.replace(/\{size\}/g, rawSize);
+ },
- if (username.match(/[^A-Za-z0-9_]/)) { return ""; }
- if (template) return template.replace(/\{size\}/g, rawSize);
- return Discourse.getURL("/users/") + username.toLowerCase() + "/avatar/" + rawSize + "?__ws=" + encodeURIComponent(Discourse.BaseUrl || "");
+ getRawSize: function(size) {
+ var pixelRatio = window.devicePixelRatio || 1;
+ return pixelRatio >= 1.5 ? size * 2 : size;
},
avatarImg: function(options) {
var size = Discourse.Utilities.translateSize(options.size);
- var url = Discourse.Utilities.avatarUrl(options.username, options.size, options.avatarTemplate);
+ var url = Discourse.Utilities.avatarUrl(options.avatarTemplate, size);
// We won't render an invalid url
if (!url || url.length === 0) { return ""; }
@@ -71,8 +74,8 @@ Discourse.Utilities = {
return " ";
},
- tinyAvatar: function(username) {
- return Discourse.Utilities.avatarImg({ username: username, size: 'tiny' });
+ tinyAvatar: function(avatarTemplate) {
+ return Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny' });
},
postUrl: function(slug, topicId, postNumber) {
@@ -266,6 +269,28 @@ Discourse.Utilities = {
authorizedExtensions: function() {
return Discourse.SiteSettings.authorized_extensions.replace(/\|/g, ", ");
+ },
+
+ displayErrorForUpload: function(data) {
+ // deal with meaningful errors first
+ if (data.jqXHR) {
+ switch (data.jqXHR.status) {
+ // cancel from the user
+ case 0: return;
+ // entity too large, usually returned from the web server
+ case 413:
+ var maxSizeKB = Discourse.SiteSettings.max_image_size_kb;
+ bootbox.alert(I18n.t('post.errors.image_too_large', { max_size_kb: maxSizeKB }));
+ return;
+ // the error message is provided by the server
+ case 415: // media type not authorized
+ case 422: // there has been an error on the server (mostly due to FastImage)
+ bootbox.alert(data.jqXHR.responseText);
+ return;
+ }
+ }
+ // otherwise, display a generic error message
+ bootbox.alert(I18n.t('post.errors.upload'));
}
};
diff --git a/app/assets/javascripts/discourse/controllers/preferences_avatar_controller.js b/app/assets/javascripts/discourse/controllers/preferences_avatar_controller.js
new file mode 100644
index 0000000000..4118049597
--- /dev/null
+++ b/app/assets/javascripts/discourse/controllers/preferences_avatar_controller.js
@@ -0,0 +1,84 @@
+/**
+ This controller supports actions related to updating one's avatar
+
+ @class PreferencesAvatarController
+ @extends Discourse.ObjectController
+ @namespace Discourse
+ @module Discourse
+**/
+Discourse.PreferencesAvatarController = Discourse.ObjectController.extend({
+ uploading: false,
+ uploadProgress: 0,
+ uploadDisabled: Em.computed.or("uploading"),
+ useGravatar: Em.computed.not("use_uploaded_avatar"),
+ useUploadedAvatar: Em.computed.alias("use_uploaded_avatar"),
+
+ toggleUseUploadedAvatar: function(toggle) {
+ if (this.get("use_uploaded_avatar") !== toggle) {
+ var controller = this;
+ this.set("use_uploaded_avatar", toggle);
+ Discourse.ajax("/users/" + this.get("username") + "/preferences/avatar/toggle", { type: 'PUT', data: { use_uploaded_avatar: toggle }})
+ .then(function(result) { controller.set("avatar_template", result.avatar_template); });
+ }
+ },
+
+ uploadButtonText: function() {
+ return this.get("uploading") ? I18n.t("user.change_avatar.uploading") : I18n.t("user.change_avatar.upload");
+ }.property("uploading"),
+
+ uploadAvatar: function() {
+ var controller = this;
+ var $upload = $("#avatar-input");
+
+ // do nothing if no file is selected
+ if (Em.isEmpty($upload.val())) { return; }
+
+ this.set("uploading", true);
+
+ // define the upload endpoint
+ $upload.fileupload({
+ url: Discourse.getURL("/users/" + this.get("username") + "/preferences/avatar"),
+ dataType: "json",
+ timeout: 20000
+ });
+
+ // when there is a progression for the upload
+ $upload.on("fileuploadprogressall", function (e, data) {
+ var progress = parseInt(data.loaded / data.total * 100, 10);
+ controller.set("uploadProgress", progress);
+ });
+
+ // when the upload is successful
+ $upload.on("fileuploaddone", function (e, data) {
+ // set some properties
+ controller.setProperties({
+ has_uploaded_avatar: true,
+ use_uploaded_avatar: true,
+ avatar_template: data.result.url,
+ uploaded_avatar_template: data.result.url
+ });
+ });
+
+ // when there has been an error with the upload
+ $upload.on("fileuploadfail", function (e, data) {
+ Discourse.Utilities.displayErrorForUpload(data);
+ });
+
+ // when the upload is done
+ $upload.on("fileuploadalways", function (e, data) {
+ // prevent automatic upload when selecting a file
+ $upload.fileupload("destroy");
+ $upload.off();
+ // clear file input
+ $upload.val("");
+ // indicate upload is done
+ controller.setProperties({
+ uploading: false,
+ uploadProgress: 0
+ });
+ });
+
+ // *actually* launch the upload
+ $("#avatar-input").fileupload("add", { fileInput: $("#avatar-input") });
+ }
+});
diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application_helpers.js
index e806482412..b941f29144 100644
--- a/app/assets/javascripts/discourse/helpers/application_helpers.js
+++ b/app/assets/javascripts/discourse/helpers/application_helpers.js
@@ -116,16 +116,22 @@ Handlebars.registerHelper('lower', function(property, options) {
@for Handlebars
**/
Handlebars.registerHelper('avatar', function(user, options) {
-
if (typeof user === 'string') {
user = Ember.Handlebars.get(this, user, options);
}
- if( user ) {
+ if (user) {
var username = Em.get(user, 'username');
if (!username) username = Em.get(user, options.hash.usernamePath);
- var avatarTemplate = Ember.get(user, 'avatar_template');
+ 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;
@@ -147,7 +153,6 @@ Handlebars.registerHelper('avatar', function(user, options) {
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize,
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
- username: username,
title: title || username,
avatarTemplate: avatarTemplate
}));
@@ -158,18 +163,32 @@ Handlebars.registerHelper('avatar', function(user, options) {
/**
Bound avatar helper.
+ Will rerender whenever the "avatar_template" changes.
@method boundAvatar
@for Handlebars
**/
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
- var username = Em.get(user, 'username');
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
size: options.hash.imageSize,
- username: username,
- avatarTemplate: Ember.get(user, 'avatar_template')
+ avatarTemplate: Em.get(user, 'avatar_template')
}));
-});
+}, 'avatar_template');
+
+/**
+ Bound avatar helper.
+ Will rerender whenever the "uploaded_avatar_template" changes.
+ Only available for the current user.
+
+ @method boundUploadedAvatar
+ @for Handlebars
+**/
+Ember.Handlebars.registerBoundHelper('boundUploadedAvatar', function(user, options) {
+ return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
+ size: options.hash.imageSize,
+ avatarTemplate: Em.get(user, 'uploaded_avatar_template')
+ }));
+}, 'uploaded_avatar_template');
/**
Nicely format a date without a binding since the date doesn't need to change.
diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js
index 72cbff882f..dbe43748da 100644
--- a/app/assets/javascripts/discourse/models/composer.js
+++ b/app/assets/javascripts/discourse/models/composer.js
@@ -70,14 +70,14 @@ Discourse.Composer = Discourse.Model.extend({
if (post) {
postDescription = I18n.t('post.' + this.get('action'), {
link: postLink,
- replyAvatar: Discourse.Utilities.tinyAvatar(post.get('username')),
+ replyAvatar: Discourse.Utilities.tinyAvatar(post.get('avatar_template')),
username: this.get('post.username')
});
var replyUsername = post.get('reply_to_user.username');
- if (replyUsername && this.get('action') === EDIT) {
- postDescription += " " + I18n.t("post.in_reply_to") + " " +
- Discourse.Utilities.tinyAvatar(replyUsername) + " " + replyUsername;
+ var replyAvatarTemplate = post.get('reply_to_user.avatar_template');
+ if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) {
+ postDescription += " " + I18n.t("post.in_reply_to") + " " + Discourse.Utilities.tinyAvatar(replyAvatarTemplate) + " " + replyUsername;
}
}
diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js
index 4cf9bf2897..d1b8426617 100644
--- a/app/assets/javascripts/discourse/models/user.js
+++ b/app/assets/javascripts/discourse/models/user.js
@@ -26,25 +26,6 @@ Discourse.User = Discourse.Model.extend({
**/
staff: Em.computed.or('admin', 'moderator'),
- /**
- Large version of this user's avatar.
-
- @property avatarLarge
- @type {String}
- **/
- avatarLarge: function() {
- return Discourse.Utilities.avatarUrl(this.get('username'), 'large', this.get('avatar_template'));
- }.property('username'),
-
- /**
- Small version of this user's avatar.
-
- @property avatarSmall
- @type {String}
- **/
- avatarSmall: function() {
- return Discourse.Utilities.avatarUrl(this.get('username'), 'small', this.get('avatar_template'));
- }.property('username'),
searchContext: function() {
return ({ type: 'user', id: this.get('username_lower'), user: this });
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index c60b937844..5eadf489a7 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -57,6 +57,7 @@ Discourse.Route.buildRoutes(function() {
this.route('username', { path: '/username' });
this.route('email', { path: '/email' });
this.route('about', { path: '/about-me' });
+ this.route('avatar', { path: '/avatar' });
});
this.route('invited', { path: 'invited' });
diff --git a/app/assets/javascripts/discourse/routes/preferences_routes.js b/app/assets/javascripts/discourse/routes/preferences_routes.js
index 6e6e14160e..55427f4b9f 100644
--- a/app/assets/javascripts/discourse/routes/preferences_routes.js
+++ b/app/assets/javascripts/discourse/routes/preferences_routes.js
@@ -116,4 +116,33 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
setupController: function(controller, user) {
controller.setProperties({ model: user, newUsername: user.get('username') });
}
-});
\ No newline at end of file
+});
+
+
+/**
+ The route for updating a user's avatar
+
+ @class PreferencesAvatarRoute
+ @extends Discourse.RestrictedUserRoute
+ @namespace Discourse
+ @module Discourse
+**/
+Discourse.PreferencesAvatarRoute = 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
+ exit: function() {
+ this._super();
+ this.render('preferences', { into: 'user', outlet: 'userOutlet', controller: 'preferences' });
+ },
+
+ setupController: function(controller, user) {
+ controller.setProperties({ model: user });
+ }
+});
diff --git a/app/assets/javascripts/discourse/templates/embedded_post.js.handlebars b/app/assets/javascripts/discourse/templates/embedded_post.js.handlebars
index a5d0be4e20..8c1e7d6f8b 100644
--- a/app/assets/javascripts/discourse/templates/embedded_post.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/embedded_post.js.handlebars
@@ -2,7 +2,7 @@
diff --git a/app/assets/javascripts/discourse/templates/user/user.js.handlebars b/app/assets/javascripts/discourse/templates/user/user.js.handlebars
index 680c6c9a8e..bfa14de944 100644
--- a/app/assets/javascripts/discourse/templates/user/user.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/user/user.js.handlebars
@@ -29,7 +29,7 @@
{{/if}}
- {{boundAvatar model imageSize="120"}}
+ {{boundAvatar model imageSize="huge"}}
diff --git a/app/assets/javascripts/discourse/views/actions_history_view.js b/app/assets/javascripts/discourse/views/actions_history_view.js
index c112ffaf77..ece98f66ed 100644
--- a/app/assets/javascripts/discourse/views/actions_history_view.js
+++ b/app/assets/javascripts/discourse/views/actions_history_view.js
@@ -38,7 +38,6 @@ Discourse.ActionsHistoryView = Discourse.View.extend({
}
iconsHtml += Discourse.Utilities.avatarImg({
size: 'small',
- username: u.get('username'),
avatarTemplate: u.get('avatar_template'),
title: u.get('username')
});
diff --git a/app/assets/javascripts/discourse/views/composer_view.js b/app/assets/javascripts/discourse/views/composer_view.js
index ed365503a4..57fada42ed 100644
--- a/app/assets/javascripts/discourse/views/composer_view.js
+++ b/app/assets/javascripts/discourse/views/composer_view.js
@@ -191,8 +191,11 @@ Discourse.ComposerView = Discourse.View.extend({
});
this.editor = editor = Discourse.Markdown.createEditor({
- lookupAvatar: function(username) {
- return Discourse.Utilities.avatarImg({ username: username, size: 'tiny' });
+ lookupAvatarByPostNumber: function(postNumber) {
+ var quotedPost = composerView.get('controller.controllers.topic.postStream.posts').findProperty("post_number", postNumber);
+ if (quotedPost) {
+ return Discourse.Utilities.tinyAvatar(quotedPost.get("avatar_template"));
+ }
}
});
@@ -295,27 +298,8 @@ Discourse.ComposerView = Discourse.View.extend({
$uploadTarget.on('fileuploadfail', function (e, data) {
// hide upload status
composerView.set('isUploading', false);
- // deal with meaningful errors first
- if (data.jqXHR) {
- switch (data.jqXHR.status) {
- // 0 == cancel from the user
- case 0: return;
- // 413 == entity too large, usually returned from the web server
- case 413:
- var type = Discourse.Utilities.isAnImage(data.files[0].name) ? "image" : "attachment";
- var maxSizeKB = Discourse.SiteSettings['max_' + type + '_size_kb'];
- bootbox.alert(I18n.t('post.errors.' + type + '_too_large', { max_size_kb: maxSizeKB }));
- return;
- // 415 == media type not authorized
- case 415:
- // 422 == there has been an error on the server (mostly due to FastImage)
- case 422:
- bootbox.alert(data.jqXHR.responseText);
- return;
- }
- }
- // otherwise, display a generic error message
- bootbox.alert(I18n.t('post.errors.upload'));
+ // display an error message
+ Discourse.Utilities.displayErrorForUpload(data);
});
// I hate to use Em.run.later, but I don't think there's a way of waiting for a CSS transition
@@ -323,11 +307,7 @@ Discourse.ComposerView = Discourse.View.extend({
return Em.run.later(jQuery, (function() {
var replyTitle = $('#reply-title');
composerView.resize();
- if (replyTitle.length) {
- return replyTitle.putCursorAtEnd();
- } else {
- return $wmdInput.putCursorAtEnd();
- }
+ return replyTitle.length ? replyTitle.putCursorAtEnd() : $wmdInput.putCursorAtEnd();
}), 300);
},
diff --git a/app/assets/javascripts/discourse/views/user/preferences_avatar_view.js b/app/assets/javascripts/discourse/views/user/preferences_avatar_view.js
new file mode 100644
index 0000000000..4eb5d6eff6
--- /dev/null
+++ b/app/assets/javascripts/discourse/views/user/preferences_avatar_view.js
@@ -0,0 +1,21 @@
+/**
+ This view handles rendering of a user's avatar uploader
+
+ @class PreferencesAvatarView
+ @extends Discourse.View
+ @namespace Discourse
+ @module Discourse
+**/
+Discourse.PreferencesAvatarView = Discourse.View.extend({
+ templateName: "user/avatar",
+ classNames: ["user-preferences"],
+
+ selectedChanged: function() {
+ var view = this;
+ Em.run.next(function() {
+ var value = view.get("controller.use_uploaded_avatar") ? "uploaded_avatar" : "gravatar";
+ view.$('input:radio[name="avatar"]').val([value]);
+ });
+ }.observes('controller.use_uploaded_avatar')
+
+});
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index a6eccb61dd..c2acad8b73 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -3,7 +3,6 @@ require_dependency 'promotion'
class TopicsController < ApplicationController
- # Avatar is an image request, not XHR
before_filter :ensure_logged_in, only: [:timings,
:destroy_timings,
:update,
@@ -22,8 +21,7 @@ class TopicsController < ApplicationController
before_filter :consider_user_for_promotion, only: :show
- skip_before_filter :check_xhr, only: [:avatar, :show, :feed]
- caches_action :avatar, cache_path: Proc.new {|c| "#{c.params[:post_number]}-#{c.params[:topic_id]}" }
+ skip_before_filter :check_xhr, only: [:show, :feed]
def show
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 4cb28ec4e2..2376901265 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,8 +3,7 @@ require_dependency 'user_name_suggester'
class UsersController < ApplicationController
- skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :activate_account, :avatar, :authorize_email, :user_preferences_redirect]
- skip_before_filter :authorize_mini_profiler, only: [:avatar]
+ skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :activate_account, :authorize_email, :user_preferences_redirect]
before_filter :ensure_logged_in, only: [:username, :update, :change_email, :user_preferences_redirect]
@@ -220,25 +219,6 @@ class UsersController < ApplicationController
render json: {value: honeypot_value, challenge: challenge_value}
end
- # all avatars are funneled through here
- def avatar
-
- # TEMP to catch all missing spots
- # raise ActiveRecord::RecordNotFound
-
- user = User.select(:email).where(username_lower: params[:username].downcase).first
- if user.present?
- # for now we only support gravatar in square (redirect cached for a day),
- # later we can use x-sendfile and/or a cdn to serve local
- size = determine_avatar_size(params[:size])
- url = user.avatar_template.gsub("{size}", size.to_s)
- expires_in 1.day
- redirect_to url
- else
- raise ActiveRecord::RecordNotFound
- end
- end
-
def password_reset
expires_now()
@@ -336,6 +316,46 @@ class UsersController < ApplicationController
methods: :avatar_template) }
end
+ def avatar
+ user = fetch_user_from_params
+ guardian.ensure_can_edit!(user)
+
+ file = params[:file] || params[:files].first
+
+ # check the file size (note: this might also be done in the web server)
+ filesize = File.size(file.tempfile)
+ max_size_kb = SiteSetting.max_image_size_kb * 1024
+ return render status: 413, text: I18n.t("upload.images.too_large", max_size_kb: max_size_kb) if filesize > max_size_kb
+
+ upload = Upload.create_for(user.id, file, filesize)
+
+ user.uploaded_avatar = upload
+ user.use_uploaded_avatar = true
+ user.save!
+
+ Jobs.enqueue(:generate_avatars, upload_id: upload.id)
+
+ render json: { url: upload.url }
+
+ rescue FastImage::ImageFetchFailure
+ render status: 422, text: I18n.t("upload.images.fetch_failure")
+ rescue FastImage::UnknownImageType
+ render status: 422, text: I18n.t("upload.images.unknown_image_type")
+ rescue FastImage::SizeNotFound
+ render status: 422, text: I18n.t("upload.images.size_not_found")
+ end
+
+ def toggle_avatar
+ params.require(:use_uploaded_avatar)
+ user = fetch_user_from_params
+ guardian.ensure_can_edit!(user)
+
+ user.use_uploaded_avatar = params[:use_uploaded_avatar]
+ user.save!
+
+ render json: { avatar_template: user.avatar_template }
+ end
+
private
def honeypot_value
@@ -405,12 +425,4 @@ class UsersController < ApplicationController
auth[:github_user_id] && auth[:github_screen_name] &&
GithubUserInfo.find_by_github_user_id(auth[:github_user_id]).nil?
end
-
- def determine_avatar_size(size)
- size = size.to_i
- size = 64 if size == 0
- size = 10 if size < 10
- size = 128 if size > 128
- size
- end
end
diff --git a/app/models/blocked_email.rb b/app/models/blocked_email.rb
index b9a3b0ac47..1aa85ec434 100644
--- a/app/models/blocked_email.rb
+++ b/app/models/blocked_email.rb
@@ -33,3 +33,22 @@ class BlockedEmail < ActiveRecord::Base
end
end
+
+# == Schema Information
+#
+# Table name: blocked_emails
+#
+# id :integer not null, primary key
+# email :string(255) not null
+# action_type :integer not null
+# match_count :integer default(0), not null
+# last_match_at :datetime
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+# Indexes
+#
+# index_blocked_emails_on_email (email) UNIQUE
+# index_blocked_emails_on_last_match_at (last_match_at)
+#
+
diff --git a/app/models/category_featured_topic.rb b/app/models/category_featured_topic.rb
index 728fbd9672..5bbdc3acae 100644
--- a/app/models/category_featured_topic.rb
+++ b/app/models/category_featured_topic.rb
@@ -44,6 +44,7 @@ end
# created_at :datetime not null
# updated_at :datetime not null
# rank :integer default(0), not null
+# id :integer not null, primary key
#
# Indexes
#
diff --git a/app/models/incoming_link.rb b/app/models/incoming_link.rb
index 42b6f6a650..0ca8d4acc8 100644
--- a/app/models/incoming_link.rb
+++ b/app/models/incoming_link.rb
@@ -106,6 +106,8 @@ end
#
# Indexes
#
-# incoming_index (topic_id,post_number)
+# incoming_index (topic_id,post_number)
+# index_incoming_links_on_created_at_and_domain (created_at,domain)
+# index_incoming_links_on_created_at_and_user_id (created_at,user_id)
#
diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb
index aee807d1e8..681059c8c2 100644
--- a/app/models/optimized_image.rb
+++ b/app/models/optimized_image.rb
@@ -43,7 +43,7 @@ class OptimizedImage < ActiveRecord::Base
def destroy
OptimizedImage.transaction do
- Discourse.store.remove_file(url)
+ Discourse.store.remove_optimized_image(self)
super
end
end
@@ -60,6 +60,7 @@ end
# width :integer not null
# height :integer not null
# upload_id :integer not null
+# url :string(255) not null
#
# Indexes
#
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 0187be422c..3405372ee2 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -242,6 +242,7 @@ class SiteSetting < ActiveRecord::Base
setting(:username_change_period, 3) # days
+ client_setting(:allow_uploaded_avatars, false)
def self.generate_api_key!
self.api_key = SecureRandom.hex(32)
diff --git a/app/models/staff_action_log.rb b/app/models/staff_action_log.rb
index 5d9e2628b8..b3312a1bda 100644
--- a/app/models/staff_action_log.rb
+++ b/app/models/staff_action_log.rb
@@ -37,5 +37,14 @@ end
# details :text
# created_at :datetime not null
# updated_at :datetime not null
+# context :string(255)
+# ip_address :string(255)
+# email :string(255)
+#
+# Indexes
+#
+# index_staff_action_logs_on_action_and_id (action,id)
+# index_staff_action_logs_on_staff_user_id_and_id (staff_user_id,id)
+# index_staff_action_logs_on_target_user_id_and_id (target_user_id,id)
#
diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb
index 185bede535..f5e2efda96 100644
--- a/app/models/topic_user.rb
+++ b/app/models/topic_user.rb
@@ -259,6 +259,7 @@ end
# total_msecs_viewed :integer default(0), not null
# cleared_pinned_at :datetime
# unstarred_at :datetime
+# id :integer not null, primary key
#
# Indexes
#
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 2b1c35f969..a7fcafecb1 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -31,11 +31,15 @@ class Upload < ActiveRecord::Base
def destroy
Upload.transaction do
- Discourse.store.remove_file(url)
+ Discourse.store.remove_upload(self)
super
end
end
+ def extension
+ File.extname(original_filename)
+ end
+
def self.create_for(user_id, file, filesize)
# compute the sha
sha1 = Digest::SHA1.file(file.tempfile).hexdigest
diff --git a/app/models/user.rb b/app/models/user.rb
index b350637af1..da795d0ab3 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -28,6 +28,7 @@ class User < ActiveRecord::Base
has_many :user_visits
has_many :invites
has_many :topic_links
+ has_many :uploads
has_one :facebook_user_info, dependent: :destroy
has_one :twitter_user_info, dependent: :destroy
@@ -41,6 +42,8 @@ class User < ActiveRecord::Base
has_one :user_search_data
+ belongs_to :uploaded_avatar, class_name: 'Upload', dependent: :destroy
+
validates_presence_of :username
validate :username_validator
validates :email, presence: true, uniqueness: true
@@ -295,24 +298,38 @@ class User < ActiveRecord::Base
end
def self.avatar_template(email)
+ user = User.select([:email, :use_uploaded_avatar, :uploaded_avatar_template, :uploaded_avatar_id])
+ .where(email: email.downcase)
+ .first
+ if user.present?
+ if SiteSetting.allow_uploaded_avatars? && user.use_uploaded_avatar
+ # the avatars might take a while to generate
+ # so return the url of the original image in the meantime
+ user.uploaded_avatar_template.present? ? user.uploaded_avatar_template : user.uploaded_avatar.url
+ else
+ User.gravatar_template(email)
+ end
+ end
+ end
+
+ def self.gravatar_template(email)
email_hash = self.email_hash(email)
- # robohash was possibly causing caching issues
- # robohash = CGI.escape("http://robohash.org/size_") << "{size}x{size}" << CGI.escape("/#{email_hash}.png")
- "https://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
# Don't pass this up to the client - it's meant for server side use
- # The only spot this is now used is for self oneboxes in open graph data
+ # This is used in
+ # - self oneboxes in open graph data
+ # - emails
def small_avatar_url
- "https://www.gravatar.com/avatar/#{email_hash}.png?s=60&r=pg&d=identicon"
+ template = User.avatar_template(email)
+ template.gsub(/\{size\}/, "60")
end
- # return null for local avatars, a template for gravatar
def avatar_template
User.avatar_template(email)
end
-
# Updates the denormalized view counts for all users
def self.update_view_counts
# Update denormalized topics_entered
@@ -506,6 +523,9 @@ class User < ActiveRecord::Base
end
end
+ def has_uploaded_avatar
+ uploaded_avatar.present?
+ end
protected
@@ -529,7 +549,6 @@ class User < ActiveRecord::Base
end
end
-
def create_email_token
email_tokens.create(email: email)
end
@@ -571,13 +590,13 @@ class User < ActiveRecord::Base
end
end
- def send_approval_email
- Jobs.enqueue(:user_email,
- type: :signup_after_approval,
- user_id: id,
- email_token: email_tokens.first.token
- )
- end
+ def send_approval_email
+ Jobs.enqueue(:user_email,
+ type: :signup_after_approval,
+ user_id: id,
+ email_token: email_tokens.first.token
+ )
+ end
private
@@ -647,6 +666,9 @@ end
# blocked :boolean default(FALSE)
# dynamic_favicon :boolean default(FALSE), not null
# title :string(255)
+# use_uploaded_avatar :boolean default(FALSE)
+# uploaded_avatar_template :string(255)
+# uploaded_avatar_id :integer
#
# Indexes
#
diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb
index 69079b4f36..59c28c7f46 100644
--- a/app/serializers/current_user_serializer.rb
+++ b/app/serializers/current_user_serializer.rb
@@ -14,7 +14,11 @@ class CurrentUserSerializer < BasicUserSerializer
:external_links_in_new_tab,
:dynamic_favicon,
:trust_level,
- :can_edit
+ :can_edit,
+ :use_uploaded_avatar,
+ :has_uploaded_avatar,
+ :gravatar_template,
+ :uploaded_avatar_template
def include_site_flagged_posts_count?
object.staff?
@@ -36,4 +40,8 @@ class CurrentUserSerializer < BasicUserSerializer
true
end
+ def gravatar_template
+ User.gravatar_template(object.email)
+ end
+
end
diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb
index 48e1cef600..09f4d9bec8 100644
--- a/app/serializers/post_serializer.rb
+++ b/app/serializers/post_serializer.rb
@@ -111,7 +111,8 @@ class PostSerializer < BasicPostSerializer
def reply_to_user
{
username: object.reply_to_user.username,
- name: object.reply_to_user.name
+ name: object.reply_to_user.name,
+ avatar_template: object.reply_to_user.avatar_template
}
end
diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb
index 3858000afb..a7a5dbec5d 100644
--- a/config/initializers/06-mini_profiler.rb
+++ b/config/initializers/06-mini_profiler.rb
@@ -20,7 +20,6 @@ if defined?(Rack::MiniProfiler)
(env['PATH_INFO'] !~ /topics\/timings/) &&
(env['PATH_INFO'] !~ /assets/) &&
(env['PATH_INFO'] !~ /qunit/) &&
- (env['PATH_INFO'] !~ /users\/.*\/avatar/) &&
(env['PATH_INFO'] !~ /srv\/status/) &&
(env['PATH_INFO'] !~ /commits-widget/)
end
diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml
index 29d798d602..57cc658473 100644
--- a/config/locales/client.cs.yml
+++ b/config/locales/client.cs.yml
@@ -327,7 +327,8 @@ cs:
title: "Poslední IP adresa"
avatar:
title: "Avatar"
- instructions: "Používáme službu Gravatar pro zobrazení avataru podle vaší emailové adresy"
+ instructions:
+ gravatar: "Používáme službu Gravatar pro zobrazení avataru podle vaší emailové adresy"
title:
title: "Nadpis"
diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml
index 6cc05652c5..3ac6a6d0d3 100644
--- a/config/locales/client.da.yml
+++ b/config/locales/client.da.yml
@@ -187,7 +187,8 @@ da:
title: "Sidste IP-adresse"
avatar:
title: "Brugerbillede"
- instructions: "Vi bruger Gravatar for brugerbilleder baseret på e-mail-adresse"
+ instructions:
+ gravatar: "Vi bruger Gravatar for brugerbilleder baseret på e-mail-adresse"
filters:
all: "Alle"
diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml
index ce3a559b9e..a3c8e9f230 100644
--- a/config/locales/client.de.yml
+++ b/config/locales/client.de.yml
@@ -308,7 +308,8 @@ de:
title: "Letzte IP-Adresse"
avatar:
title: "Avatar"
- instructions: "Wir nutzen Gravatar zur Darstellung von Avataren basierend auf deiner Mailadresse:"
+ instructions:
+ gravatar: "Wir nutzen Gravatar zur Darstellung von Avataren basierend auf deiner Mailadresse:"
title:
title: "Title"
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index a2cdd55d58..09400690e5 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -132,6 +132,10 @@ en:
saving: "Saving..."
saved: "Saved!"
+ upload: "Upload"
+ uploading: "Uploading..."
+ uploaded: "Uploaded!"
+
choose_topic:
none_found: "No topics found."
title:
@@ -211,6 +215,15 @@ en:
error: "There was an error changing your email. Perhaps that address is already in use?"
success: "We've sent an email to that address. Please follow the confirmation instructions."
+ change_avatar:
+ title: "Change your avatar"
+ upload_instructions: "Or you could upload an image"
+ upload: "Upload a picture"
+ uploading: "Uploading the picture..."
+ gravatar: "Gravatar"
+ gravatar_title: "Change your avatar on Gravatar's website"
+ uploaded_avatar: "Uploaded picture"
+
email:
title: "Email"
instructions: "Your email will never be shown to the public."
@@ -304,7 +317,9 @@ en:
title: "Last IP Address"
avatar:
title: "Avatar"
- instructions: "We use Gravatar for avatars based on your email"
+ instructions:
+ gravatar: "We use Gravatar for avatars based on your email:"
+ uploaded_avatar: "We use the avatar you uploaded."
title:
title: "Title"
diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml
index eaf62f1e3b..fbdefd0591 100644
--- a/config/locales/client.es.yml
+++ b/config/locales/client.es.yml
@@ -263,7 +263,8 @@ es:
title: "Última Dirección IP"
avatar:
title: "Avatar"
- instructions: "Usamos Gravatar para obtener tu avatar basado en tu dirección de email."
+ instructions:
+ gravatar: "Usamos Gravatar para obtener tu avatar basado en tu dirección de email."
filters:
all: "Todos"
diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml
index bc6dca4877..d69dd9feb0 100644
--- a/config/locales/client.fr.yml
+++ b/config/locales/client.fr.yml
@@ -138,6 +138,10 @@ fr:
saving: "Sauvegarde en cours..."
saved: "Sauvegardé !"
+ save: "Envoyer"
+ saving: "Envois en cours..."
+ saved: "Envoyé !"
+
choose_topic:
none_found: "Aucune discussion trouvée."
title:
@@ -215,6 +219,14 @@ fr:
error: "Il y a eu une erreur lors du changement d'email. Cette adresse est peut-être déjà utilisée ?"
success: "Nous vous avons envoyé un mail à cette adresse. Merci de suivre les instructions."
+ change_avatar:
+ title: "Changez votre avatar"
+ upload_instructions: "Ou vous pourriez envoyer une image"
+ upload: "Envoyer une image"
+ uploading: "Image en cours d'envois..."
+ gravatar: "Gravatar"
+ uploaded_avatar: "Image envoyée"
+
email:
title: "Email"
instructions: "Votre adresse email ne sera jamais comuniquée."
@@ -309,8 +321,10 @@ fr:
title: "Dernières adresses IP"
avatar:
title: "Avatar"
- instructions: "Nous utilisons Gravatar pour associer votre avatar avec votre adresse email."
-
+ instructions:
+ gravatar: "Nous utilisons Gravatar pour associer votre avatar avec votre adresse email :"
+ uploaded_avatar: "Nous utilisons l'avatar que vous avez envoyé."
+ upload: "Ou vous pouvez envoyer une image"
filters:
all: "Tout"
diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml
index 6d1c38133c..f4f86754ff 100644
--- a/config/locales/client.id.yml
+++ b/config/locales/client.id.yml
@@ -181,7 +181,8 @@ id:
title: "Last IP Address"
avatar:
title: "Avatar"
- instructions: "We use Gravatar for avatars based on your email"
+ instructions:
+ gravatar: "We use Gravatar for avatars based on your email"
filters:
all: "All"
diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml
index 3a8ddc7ec9..5d92f69520 100644
--- a/config/locales/client.it.yml
+++ b/config/locales/client.it.yml
@@ -247,7 +247,8 @@ it:
title: "Ultimo indirizzo IP"
avatar:
title: "Avatar"
- instructions: "Usiamo Gravatar per gli avatar basandoci sulla tua email"
+ instructions:
+ gravatar: "Usiamo Gravatar per gli avatar basandoci sulla tua email"
filters:
all: "Tutti"
diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml
index 6012a09773..db347b288e 100644
--- a/config/locales/client.ko.yml
+++ b/config/locales/client.ko.yml
@@ -213,7 +213,8 @@ ko:
title: "마지막 IP 주소"
avatar:
title: "아바타"
- instructions: "포럼에서는 당신의 Email을 기바능로 Gravatar 아바타 서비스를 기본적으로 사용합니다"
+ instructions:
+ gravatar: "포럼에서는 당신의 Email을 기바능로 Gravatar 아바타 서비스를 기본적으로 사용합니다"
filters:
all: "All"
diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml
index 1125b1bf6b..64f5dc2486 100644
--- a/config/locales/client.nb_NO.yml
+++ b/config/locales/client.nb_NO.yml
@@ -247,7 +247,8 @@ nb_NO:
title: "Siste IP Addresse"
avatar:
title: "Profilbilde"
- instructions: "Vi bruker Gravatar basert på din email for profilbilder."
+ instructions:
+ gravatar: "Vi bruker Gravatar basert på din email for profilbilder."
filters:
all: "Alle"
diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml
index 024e073854..735c595b06 100644
--- a/config/locales/client.nl.yml
+++ b/config/locales/client.nl.yml
@@ -305,7 +305,8 @@ nl:
title: Laatste IP-adres
avatar:
title: Profielfoto
- instructions: "Wij gebruiken Gravatar voor profielfoto's die aan je e-mailadres gekoppeld zijn"
+ instructions:
+ gravatar: "Wij gebruiken Gravatar voor profielfoto's die aan je e-mailadres gekoppeld zijn"
title:
title: Titel
diff --git a/config/locales/client.pseudo.yml b/config/locales/client.pseudo.yml
index 308b86597e..592f108916 100644
--- a/config/locales/client.pseudo.yml
+++ b/config/locales/client.pseudo.yml
@@ -275,7 +275,8 @@ pseudo:
title: '[[ Łášť ÍР Áďďřéšš ]]'
avatar:
title: '[[ Áνáťář ]]'
- instructions: '[[ Ŵé ůšé <á ĥřéƒ=''ĥťťƿš://ǧřáνáťář.čóɱ'' ťářǧéť=''_ƀłáɳǩ''>Ǧřáνáťářá>
+ instructions:
+ gravatar: '[[ Ŵé ůšé <á ĥřéƒ=''ĥťťƿš://ǧřáνáťář.čóɱ'' ťářǧéť=''_ƀłáɳǩ''>Ǧřáνáťářá>
ƒóř áνáťářš ƀášéď óɳ ýóůř éɱáíł ]]'
title:
title: '[[ Ťíťłé ]]'
diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml
index 95facfd68b..69e302cb09 100644
--- a/config/locales/client.pt.yml
+++ b/config/locales/client.pt.yml
@@ -181,7 +181,7 @@ pt:
title: "Último endereço IP"
avatar:
title: "Avatar"
- instructions: "Nós utilizamos Gravatar para os avatares baseados no teu email"
+ instructions_gravatar: "Nós utilizamos Gravatar para os avatares baseados no teu email"
filters:
all: "Todos"
diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml
index 61da002ef3..86167d5bb9 100644
--- a/config/locales/client.pt_BR.yml
+++ b/config/locales/client.pt_BR.yml
@@ -268,7 +268,8 @@ pt_BR:
title: "Último endereço IP"
avatar:
title: "Avatar"
- instructions: "Nós utilizamos Gravatar para os avatares baseados no seu email"
+ instructions:
+ gravatar: "Nós utilizamos Gravatar para os avatares baseados no seu email"
title:
title: "Título"
filters:
diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml
index d0c5f6c34f..5d35e73020 100644
--- a/config/locales/client.ru.yml
+++ b/config/locales/client.ru.yml
@@ -328,7 +328,8 @@ ru:
title: Последний IP адрес
avatar:
title: Аватар
- instructions: "Сервис Gravatar позволяет создать аватар для вашего адреса электронной почты"
+ instructions:
+ gravatar: "Сервис Gravatar позволяет создать аватар для вашего адреса электронной почты"
title:
title: Заголовок
filters:
diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml
index 84e2c6ca0c..f1b67562c0 100644
--- a/config/locales/client.sv.yml
+++ b/config/locales/client.sv.yml
@@ -191,7 +191,8 @@ sv:
title: "Senaste IP-adress"
avatar:
title: "Profilbild"
- instructions: "Vi använder Gravatar för profilbilder baserat på din e-post"
+ instructions:
+ gravatar: "Vi använder Gravatar för profilbilder baserat på din e-post"
filters:
all: "Alla"
diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml
index 6055b09c34..69612c170d 100644
--- a/config/locales/client.zh_CN.yml
+++ b/config/locales/client.zh_CN.yml
@@ -311,7 +311,8 @@ zh_CN:
title: "最后使用的IP地址"
avatar:
title: "头像"
- instructions: "我们目前使用 Gravatar 来基于你的邮箱生成头像"
+ instructions:
+ gravatar: "我们目前使用 Gravatar 来基于你的邮箱生成头像"
title:
title: "头衔"
diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml
index 5f049901ea..7fe7cb1aee 100644
--- a/config/locales/client.zh_TW.yml
+++ b/config/locales/client.zh_TW.yml
@@ -196,7 +196,7 @@ zh_TW:
success: "(電子郵件已發送)"
in_progress: "(正在發送電子郵件)"
error: "(錯誤)"
-
+
change_about:
title: "更改關於我"
@@ -307,7 +307,8 @@ zh_TW:
title: "最後使用的IP地址"
avatar:
title: "頭像"
- instructions: "我們目前使用 Gravatar 來基於你的郵箱生成頭像"
+ instructions:
+ gravatar: "我們目前使用 Gravatar 來基於你的郵箱生成頭像"
filters:
all: "全部"
@@ -492,7 +493,7 @@ zh_TW:
placeholder: "在此輸入你的搜索條件"
no_results: "沒有找到結果。"
searching: "搜索中……"
-
+
prefer:
user: "搜索會優先列出@{{username}}的結果"
category: "搜索會優先列出{{category}}的結果"
@@ -729,7 +730,7 @@ zh_TW:
upload_not_authorized: "抱歉, 你上傳的文件並不允許 (authorized extension: {{authorized_extensions}})."
image_upload_not_allowed_for_new_user: "抱歉, 新用戶不能上傳圖片。"
attachment_upload_not_allowed_for_new_user: "抱歉, 新用戶不能上傳附件。"
-
+
abandon: "你確定要丟棄你的帖子嗎?"
archetypes:
@@ -969,7 +970,7 @@ zh_TW:
help: "在 {{categoryName}} 分類中熱門的主題"
browser_update: '抱歉, 你的瀏覽器版本太低,推薦使用Google Chrome . 請 升級你的浏覽器 。'
-
+
permission_types:
full: "創建 / 回復 / 觀看"
create_post: "回復 / 觀看"
@@ -1043,7 +1044,7 @@ zh_TW:
error: "出錯了"
view_message: "查看消息"
no_results: "沒有任何投訴"
-
+
summary:
action_type_3:
one: "離題"
@@ -1114,7 +1115,7 @@ zh_TW:
text: "文字"
last_seen_user: "最後看見用戶:"
reply_key: "回複金鑰"
-
+
logs:
title: "記錄"
action: "行動"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 190192c58c..6662beb416 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -665,6 +665,8 @@ en:
delete_all_posts_max: "The maximum number of posts that can be deleted at once with the Delete All Posts button. If a user has more than this many posts, the posts cannot all be deleted at once and the user can't be deleted."
username_change_period: "The number of days after registration that accounts can change their username."
+ allow_uploaded_avatars: "Allow support for uploaded avatars"
+
notification_types:
mentioned: "%{display_username} mentioned you in %{link}"
liked: "%{display_username} liked your post in %{link}"
diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml
index e9ceb93200..cbb51c49c2 100644
--- a/config/locales/server.fr.yml
+++ b/config/locales/server.fr.yml
@@ -613,6 +613,8 @@ fr:
minimum_topics_similar: "Combien de topics ont besoin d'exister dans la base de données avant que des topics similaires soit présentés."
+ allow_uploaded_avatars: "Permet aux utilisateurs d'uploader leur propre avatar"
+
notification_types:
mentioned: "%{display_username} vous a mentionné dans %{link}"
liked: "%{display_username} a aimé votre message dans %{link}"
diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf
index d89fd35125..240ca45a20 100644
--- a/config/nginx.sample.conf
+++ b/config/nginx.sample.conf
@@ -30,12 +30,6 @@ server {
# }
#}
- location ~ ^/t\/[0-9]+\/[0-9]+\/avatar {
- expires 1d;
- add_header Cache-Control public;
- add_header ETag "";
- }
-
location ~ ^/(assets|uploads)/ {
expires 1y;
add_header Cache-Control public;
diff --git a/config/routes.rb b/config/routes.rb
index 406e2e6a6a..53d1a66864 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -135,7 +135,9 @@ Discourse::Application.routes.draw do
get 'users/:username/preferences/about-me' => '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}
- get 'users/:username/avatar(/:size)' => 'users#avatar', constraints: {username: USERNAME_ROUTE_FORMAT}
+ get 'users/:username/preferences/avatar' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}
+ put 'users/:username/preferences/avatar/toggle' => 'users#toggle_avatar', constraints: {username: USERNAME_ROUTE_FORMAT}
+ post 'users/:username/preferences/avatar' => 'users#avatar', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'users/:username/invited' => 'users#invited', constraints: {username: USERNAME_ROUTE_FORMAT}
post 'users/:username/send_activation_email' => 'users#send_activation_email', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'users/:username/activity' => 'users#show', constraints: {username: USERNAME_ROUTE_FORMAT}
@@ -143,7 +145,6 @@ Discourse::Application.routes.draw do
resources :uploads
-
get 'posts/by_number/:topic_id/:post_number' => 'posts#by_number'
get 'posts/:id/reply-history' => 'posts#reply_history'
resources :posts do
@@ -211,9 +212,6 @@ Discourse::Application.routes.draw do
get 'topics/similar_to'
get 'topics/created-by/:username' => 'list#topics_by', as: 'topics_by', constraints: {username: USERNAME_ROUTE_FORMAT}
- # Legacy route for old avatars
- get 'threads/:topic_id/:post_number/avatar' => 'topics#avatar', constraints: {topic_id: /\d+/, post_number: /\d+/}
-
# Topic routes
get 't/:slug/:topic_id/wordpress' => 'topics#wordpress', constraints: {topic_id: /\d+/}
get 't/:slug/:topic_id/moderator-liked' => 'topics#moderator_liked', constraints: {topic_id: /\d+/}
diff --git a/db/migrate/20130809211409_add_avatar_to_users.rb b/db/migrate/20130809211409_add_avatar_to_users.rb
new file mode 100644
index 0000000000..4d570aeee7
--- /dev/null
+++ b/db/migrate/20130809211409_add_avatar_to_users.rb
@@ -0,0 +1,7 @@
+class AddAvatarToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :use_uploaded_avatar, :boolean, default: false
+ add_column :users, :uploaded_avatar_template, :string
+ add_column :users, :uploaded_avatar_id, :integer
+ end
+end
diff --git a/lib/file_store/local_store.rb b/lib/file_store/local_store.rb
index 01bdf3516e..2b8ea166ce 100644
--- a/lib/file_store/local_store.rb
+++ b/lib/file_store/local_store.rb
@@ -1,41 +1,31 @@
class LocalStore
def store_upload(file, upload)
- unique_sha1 = Digest::SHA1.hexdigest("#{Time.now.to_s}#{file.original_filename}")[0,16]
- extension = File.extname(file.original_filename)
- clean_name = "#{unique_sha1}#{extension}"
- path = "#{relative_base_url}/#{upload.id}/#{clean_name}"
- # copy the file to the right location
- copy_file(file, "#{public_dir}#{path}")
- # url
- Discourse.base_uri + path
+ path = get_path_for_upload(file, upload)
+ store_file(file, path)
end
def store_optimized_image(file, optimized_image)
- # 1234567890ABCDEF_100x200.jpg
- filename = [
- optimized_image.sha1[6..16],
- "_#{optimized_image.width}x#{optimized_image.height}",
- optimized_image.extension,
- ].join
- # /public/uploads/site/_optimized/123/456/
- path = File.join(
- relative_base_url,
- "_optimized",
- optimized_image.sha1[0..2],
- optimized_image.sha1[3..5],
- filename
- )
- # copy the file to the right location
- copy_file(file, "#{public_dir}#{path}")
- # url
- Discourse.base_uri + path
+ path = get_path_for_optimized_image(file, optimized_image)
+ store_file(file, path)
end
- def remove_file(url)
- File.delete("#{public_dir}#{url}") if has_been_uploaded?(url)
- rescue Errno::ENOENT
- # don't care if the file isn't there
+ def store_avatar(file, upload, size)
+ path = get_path_for_avatar(file, upload, size)
+ store_file(file, path)
+ end
+
+ def remove_upload(upload)
+ remove_file(upload.url)
+ end
+
+ def remove_optimized_image(optimized_image)
+ remove_file(optimized_image.url)
+ end
+
+ def remove_avatars(upload)
+ return unless upload.url =~ /avatars/
+ remove_directory(File.dirname(upload.url))
end
def has_been_uploaded?(url)
@@ -63,8 +53,63 @@ class LocalStore
"#{public_dir}#{upload.url}"
end
+ def absolute_avatar_template(upload)
+ avatar_template(upload, absolute_base_url)
+ end
+
private
+ def get_path_for_upload(file, upload)
+ unique_sha1 = Digest::SHA1.hexdigest("#{Time.now.to_s}#{file.original_filename}")[0..15]
+ extension = File.extname(file.original_filename)
+ clean_name = "#{unique_sha1}#{extension}"
+ # path
+ "#{relative_base_url}/#{upload.id}/#{clean_name}"
+ end
+
+ def get_path_for_optimized_image(file, optimized_image)
+ # 1234567890ABCDEF_100x200.jpg
+ filename = [
+ optimized_image.sha1[6..15],
+ "_#{optimized_image.width}x#{optimized_image.height}",
+ optimized_image.extension,
+ ].join
+ # /uploads//_optimized/<1A3>//
+ File.join(
+ relative_base_url,
+ "_optimized",
+ optimized_image.sha1[0..2],
+ optimized_image.sha1[3..5],
+ filename
+ )
+ end
+
+ def get_path_for_avatar(file, upload, size)
+ relative_avatar_template(upload).gsub(/\{size\}/, size.to_s)
+ end
+
+ def relative_avatar_template(upload)
+ avatar_template(upload, relative_base_url)
+ end
+
+ def avatar_template(upload, base_url)
+ File.join(
+ base_url,
+ "avatars",
+ upload.sha1[0..2],
+ upload.sha1[3..5],
+ upload.sha1[6..15],
+ "{size}#{upload.extension}"
+ )
+ end
+
+ def store_file(file, path)
+ # copy the file to the right location
+ copy_file(file, "#{public_dir}#{path}")
+ # url
+ Discourse.base_uri + path
+ end
+
def copy_file(file, path)
FileUtils.mkdir_p Pathname.new(path).dirname
# move the file to the right location
@@ -74,6 +119,17 @@ class LocalStore
end
end
+ def remove_file(url)
+ File.delete("#{public_dir}#{url}") if has_been_uploaded?(url)
+ rescue Errno::ENOENT
+ # don't care if the file isn't there
+ end
+
+ def remove_directory(path)
+ directory = "#{public_dir}/#{path}"
+ FileUtils.rm_rf(directory)
+ end
+
def is_relative?(url)
url.start_with?(relative_base_url)
end
diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb
index e864591d86..02f9a29c7a 100644
--- a/lib/file_store/s3_store.rb
+++ b/lib/file_store/s3_store.rb
@@ -4,34 +4,60 @@ require 'open-uri'
class S3Store
def store_upload(file, upload)
- extension = File.extname(file.original_filename)
- remote_filename = "#{upload.id}#{upload.sha1}#{extension}"
+ #
+ path = "#{upload.id}#{upload.sha1}#{upload.extension}"
# if this fails, it will throw an exception
- upload(file.tempfile, remote_filename, file.content_type)
+ upload(file.tempfile, path, file.content_type)
# returns the url of the uploaded file
- "#{absolute_base_url}/#{remote_filename}"
+ "#{absolute_base_url}/#{path}"
end
def store_optimized_image(file, optimized_image)
- extension = File.extname(file.path)
- remote_filename = [
+ # _x
+ path = [
optimized_image.id,
optimized_image.sha1,
"_#{optimized_image.width}x#{optimized_image.height}",
- extension
+ optimized_image.extension
].join
# if this fails, it will throw an exception
- upload(file, remote_filename)
+ upload(file, path)
# returns the url of the uploaded file
- "#{absolute_base_url}/#{remote_filename}"
+ "#{absolute_base_url}/#{path}"
+ end
+
+ def store_avatar(file, upload, size)
+ # /avatars//200.jpg
+ path = File.join(
+ "avatars",
+ upload.sha1,
+ "#{size}#{upload.extension}"
+ )
+
+ # if this fails, it will throw an exception
+ upload(file, path)
+
+ # returns the url of the avatar
+ "#{absolute_base_url}/#{path}"
+ end
+
+ def remove_upload(upload)
+ remove_file(upload.url)
+ end
+
+ def remove_optimized_image(optimized_image)
+ remove_file(optimized_image.url)
+ end
+
+ def remove_avatars(upload)
+
end
def remove_file(url)
- check_missing_site_settings
return unless has_been_uploaded?(url)
name = File.basename(url)
remove(name)
diff --git a/lib/jobs/generate_avatars.rb b/lib/jobs/generate_avatars.rb
new file mode 100644
index 0000000000..9be32b3cb7
--- /dev/null
+++ b/lib/jobs/generate_avatars.rb
@@ -0,0 +1,42 @@
+require "image_sorcery"
+
+module Jobs
+
+ class GenerateAvatars < Jobs::Base
+
+ def execute(args)
+ upload = Upload.where(id: args[:upload_id]).first
+ return unless upload.present?
+
+ 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
+
+ [120, 45, 32, 25, 20].each do |s|
+ # handle retina too
+ [s, s * 2].each do |size|
+ # create a temp file with the same extension as the original
+ temp_file = Tempfile.new(["discourse-avatar", File.extname(original_path)])
+ temp_path = temp_file.path
+ #
+ Discourse.store.store_avatar(temp_file, upload, size) if ImageSorcery.new(original_path).convert(temp_path, gravity: "center", thumbnail: "#{size}x#{size}^", extent: "#{size}x#{size}")
+ # close && remove temp file
+ temp_file.close!
+ end
+ end
+
+ # make sure we remove the cached copy from external stores
+ external_copy.close! if Discourse.store.external?
+
+ user = User.where(id: upload.user_id).first
+ user.uploaded_avatar_template = Discourse.store.absolute_avatar_template(upload)
+ user.save!
+
+ end
+
+ end
+
+end
diff --git a/lib/oneboxer/discourse_local_onebox.rb b/lib/oneboxer/discourse_local_onebox.rb
index c339c16264..3d500acb92 100644
--- a/lib/oneboxer/discourse_local_onebox.rb
+++ b/lib/oneboxer/discourse_local_onebox.rb
@@ -23,7 +23,7 @@ module Oneboxer
return @url unless Guardian.new.can_see?(user)
- args.merge! avatar: PrettyText.avatar_img(user.username, 'tiny'), username: user.username
+ args.merge! avatar: PrettyText.avatar_img(user.avatar_template, 'tiny'), username: user.username
args[:bio] = user.bio_cooked if user.bio_cooked.present?
@template = 'user'
@@ -58,7 +58,7 @@ module Oneboxer
posters = topic.posters_summary.map do |p|
{username: p[:user][:username],
- avatar: PrettyText.avatar_img(p[:user][:username], 'tiny'),
+ avatar: PrettyText.avatar_img(p[:user][:avatar_template], 'tiny'),
description: p[:description],
extras: p[:extras]}
end
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 9d20a022ab..b1329ee7f6 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -64,7 +64,7 @@ module PrettyText
return "" unless username
user = User.where(username_lower: username.downcase).first
- if user
+ if user.present?
user.avatar_template
end
end
@@ -139,7 +139,7 @@ module PrettyText
v8['opts'] = opts || {}
v8['raw'] = text
v8.eval('opts["mentionLookup"] = function(u){return helpers.is_username_valid(u);}')
- v8.eval('opts["lookupAvatar"] = function(p){return Discourse.Utilities.avatarImg({username: p, size: "tiny", avatarTemplate: helpers.avatar_template(p)});}')
+ v8.eval('opts["lookupAvatar"] = function(p){return Discourse.Utilities.avatarImg({size: "tiny", avatarTemplate: helpers.avatar_template(p)});}')
baked = v8.eval('Discourse.Markdown.markdownConverter(opts).makeHtml(raw)')
end
@@ -149,15 +149,15 @@ module PrettyText
end
# leaving this here, cause it invokes v8, don't want to implement twice
- def self.avatar_img(username, size)
+ def self.avatar_img(avatar_template, size)
r = nil
@mutex.synchronize do
- v8['username'] = username
+ v8['avatarTemplate'] = avatar_template
v8['size'] = size
v8.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
v8.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
v8.eval("Discourse.BaseUrl = '#{RailsMultisite::ConnectionManagement.current_hostname}';")
- r = v8.eval("Discourse.Utilities.avatarImg({ username: username, size: size });")
+ r = v8.eval("Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size });")
end
r
end
diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb
index bd9d1f5778..b0339b8883 100644
--- a/spec/components/cooked_post_processor_spec.rb
+++ b/spec/components/cooked_post_processor_spec.rb
@@ -135,7 +135,7 @@ describe CookedPostProcessor do
it "generates overlay information" do
cpp.post_process_images
- cpp.html.should match_html '
+ cpp.html.should match_html '
'
cpp.should be_dirty
diff --git a/spec/components/file_store/local_store_spec.rb b/spec/components/file_store/local_store_spec.rb
index aca9e4b48e..684a9a273b 100644
--- a/spec/components/file_store/local_store_spec.rb
+++ b/spec/components/file_store/local_store_spec.rb
@@ -35,25 +35,38 @@ describe LocalStore do
it "returns a relative url" do
store.expects(:copy_file)
- store.store_optimized_image({}, optimized_image).should == "/uploads/default/_optimized/86f/7e4/37faa5a7fce_100x200.png"
+ store.store_optimized_image({}, optimized_image).should == "/uploads/default/_optimized/86f/7e4/37faa5a7fc_100x200.png"
end
end
- describe "remove_file" do
+ describe "remove_upload" do
- it "does not delete any file" do
+ it "does not delete non uploaded" do
File.expects(:delete).never
- store.remove_file("/path/to/file")
+ upload = Upload.new
+ upload.stubs(:url).returns("/path/to/file")
+ store.remove_upload(upload)
end
it "deletes the file locally" do
File.expects(:delete)
- store.remove_file("/uploads/default/42/253dc8edf9d4ada1.png")
+ upload = Upload.new
+ upload.stubs(:url).returns("/uploads/default/42/253dc8edf9d4ada1.png")
+ store.remove_upload(upload)
end
end
+ describe "remove_optimized_image" do
+
+ end
+
+ describe "remove_avatar" do
+
+ end
+
+
describe "has_been_uploaded?" do
it "identifies local or relatives urls" do
diff --git a/spec/components/file_store/s3_store_spec.rb b/spec/components/file_store/s3_store_spec.rb
index 8b38a4d916..fc43173f5d 100644
--- a/spec/components/file_store/s3_store_spec.rb
+++ b/spec/components/file_store/s3_store_spec.rb
@@ -40,6 +40,7 @@ describe S3Store do
it "returns a relative url" do
upload.stubs(:id).returns(42)
+ upload.stubs(:extension).returns(".png")
store.store_upload(uploaded_file, upload).should == "//s3_upload_bucket.s3.amazonaws.com/42e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98.png"
end
@@ -54,20 +55,32 @@ describe S3Store do
end
- describe "remove_file" do
+ describe "remove_upload" do
- it "does not delete any file" do
+ it "does not delete non uploaded file" do
store.expects(:remove).never
- store.remove_file("//other_bucket.s3.amazonaws.com/42.png")
+ upload = Upload.new
+ upload.stubs(:url).returns("//other_bucket.s3.amazonaws.com/42.png")
+ store.remove_upload(upload)
end
it "deletes the file on s3" do
store.expects(:remove)
- store.remove_file("//s3_upload_bucket.s3.amazonaws.com/42.png")
+ upload = Upload.new
+ upload.stubs(:url).returns("//s3_upload_bucket.s3.amazonaws.com/42.png")
+ store.remove_upload(upload)
end
end
+ describe "remove_optimized_image" do
+
+ end
+
+ describe "remove_avatar" do
+
+ end
+
describe "has_been_uploaded?" do
it "identifies S3 uploads" do
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 10440313e1..41c8ff3a91 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -628,7 +628,7 @@ describe Guardian do
Guardian.new(nil).can_see_flags?(post).should be_false
end
- it "allow regular uses to see flags" do
+ it "allow regular users to see flags" do
Guardian.new(user).can_see_flags?(post).should be_false
end
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index 3ce512cb7e..6fe8c978e9 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -14,18 +14,27 @@ test
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]").should =~ /\[sam\]/
end
- it "produces a quote even with new lines in it" do
- PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "
\n
\n
\n EvilTrout said:\n
\n ddd \n
"
- end
+ describe "with avatar" do
- it "should produce a quote" do
- PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]").should match_html "
\n
\n
\n EvilTrout said:\n
\n ddd \n
"
- end
+ before(:each) do
+ eviltrout = User.new
+ eviltrout.stubs(:avatar_template).returns("http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png")
+ User.expects(:where).with(username_lower: "eviltrout").returns([eviltrout])
+ end
- it "trims spaces on quote params" do
- PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]").should match_html "
\n
\n
\n EvilTrout said:\n
\n ddd \n
"
- end
+ it "produces a quote even with new lines in it" do
+ PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "
\n
\n
\n EvilTrout said:\n
\n ddd \n
"
+ end
+ it "should produce a quote" do
+ PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]").should match_html "
\n
\n
\n EvilTrout said:\n
\n ddd \n
"
+ end
+
+ it "trims spaces on quote params" do
+ PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]").should match_html "
\n
\n
\n EvilTrout said:\n
\n ddd \n
"
+ end
+
+ end
it "should handle 3 mentions in a row" do
PrettyText.cook('@hello @hello @hello').should match_html "
@hello @hello @hello
"
diff --git a/spec/models/optimized_image_spec.rb b/spec/models/optimized_image_spec.rb
index b2d2db798d..743c3bb0fc 100644
--- a/spec/models/optimized_image_spec.rb
+++ b/spec/models/optimized_image_spec.rb
@@ -19,7 +19,7 @@ describe OptimizedImage do
oi.extension.should == ".jpg"
oi.width.should == 100
oi.height.should == 200
- oi.url.should == "/uploads/default/_optimized/da3/9a3/ee5e6b4b0d3_100x200.jpg"
+ oi.url.should == "/uploads/default/_optimized/da3/9a3/ee5e6b4b0d_100x200.jpg"
end
end
diff --git a/test/javascripts/components/bbcode_test.js b/test/javascripts/components/bbcode_test.js
index 5941ed812d..a80a2bfec2 100644
--- a/test/javascripts/components/bbcode_test.js
+++ b/test/javascripts/components/bbcode_test.js
@@ -2,7 +2,6 @@
module("Discourse.BBCode");
var format = function(input, expected, text) {
-
var cooked = Discourse.Markdown.cook(input, {lookupAvatar: false});
equal(cooked, "
" + expected + "
", text);
};
diff --git a/test/javascripts/components/utilities_test.js b/test/javascripts/components/utilities_test.js
index ec4ac8cfbf..a0dae30b8e 100644
--- a/test/javascripts/components/utilities_test.js
+++ b/test/javascripts/components/utilities_test.js
@@ -110,33 +110,25 @@ test("isAnImage", function() {
});
test("avatarUrl", function() {
- blank(Discourse.Utilities.avatarUrl('', 'tiny'), "no avatar url returns blank");
- blank(Discourse.Utilities.avatarUrl('this is not a username', 'tiny'), "invalid username returns blank");
-
- equal(Discourse.Utilities.avatarUrl('eviltrout', 'tiny'), "/users/eviltrout/avatar/20?__ws=", "simple avatar url");
- equal(Discourse.Utilities.avatarUrl('eviltrout', 'large'), "/users/eviltrout/avatar/45?__ws=", "different size");
- equal(Discourse.Utilities.avatarUrl('EvilTrout', 'tiny'), "/users/eviltrout/avatar/20?__ws=", "lowercases username");
- equal(Discourse.Utilities.avatarUrl('eviltrout', 'tiny', 'test{size}'), "test20", "replaces the size in a template");
-});
-
-test("avatarUrl with a baseUrl", function() {
- Discourse.BaseUrl = "http://try.discourse.org";
- equal(Discourse.Utilities.avatarUrl('eviltrout', 'tiny'), "/users/eviltrout/avatar/20?__ws=http%3A%2F%2Ftry.discourse.org", "simple avatar url");
+ blank(Discourse.Utilities.avatarUrl('', 'tiny'), "no template returns blank");
+ equal(Discourse.Utilities.avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/20.png", "simple avatar url");
+ equal(Discourse.Utilities.avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/45.png", "different size");
});
test("avatarImg", function() {
- equal(Discourse.Utilities.avatarImg({username: 'eviltrout', size: 'tiny'}),
- "
",
+ var avatarTemplate = "/path/to/avatar/{size}.png";
+ equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}),
+ "
",
"it returns the avatar html");
- equal(Discourse.Utilities.avatarImg({username: 'eviltrout', size: 'tiny', title: 'evilest trout'}),
- "
",
+ equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}),
+ "
",
"it adds a title if supplied");
- equal(Discourse.Utilities.avatarImg({username: 'eviltrout', size: 'tiny', extraClasses: 'evil fish'}),
- "
",
+ equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}),
+ "
",
"it adds extra classes if supplied");
- blank(Discourse.Utilities.avatarImg({username: 'weird*username', size: 'tiny'}),
- "it doesn't render avatars for invalid usernames");
+ blank(Discourse.Utilities.avatarImg({avatarTemplate: "", size: 'tiny'}),
+ "it doesn't render avatars for invalid avatar template");
});