mirror of
https://github.com/discourse/discourse.git
synced 2024-11-27 03:10:46 -06:00
custom avatar support
This commit is contained in:
parent
e5e3164ea1
commit
c867b67a0b
@ -252,12 +252,22 @@ Discourse.BBCode = {
|
||||
// remove leading <br>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
|
||||
|
@ -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 + "</a>";
|
||||
},
|
||||
|
||||
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 "<img width='" + size + "' height='" + size + "' src='" + url + "' class='" + classes + "'" + title + ">";
|
||||
},
|
||||
|
||||
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'));
|
||||
}
|
||||
|
||||
};
|
||||
|
@ -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") });
|
||||
}
|
||||
});
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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' });
|
||||
|
@ -116,4 +116,33 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
|
||||
setupController: function(controller, user) {
|
||||
controller.setProperties({ model: user, newUsername: user.get('username') });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
@ -30,7 +30,7 @@
|
||||
{{#unless showExtraInfo}}
|
||||
<div class='current-username'>
|
||||
{{#if currentUser}}
|
||||
<span class='username'><a {{bindAttr href="currentUser.path"}}>{{currentUser.name}}</a></span>
|
||||
<span class='username'><a {{bindAttr href="currentUser.path"}}>{{currentUser.name}}</a></span>
|
||||
{{else}}
|
||||
<button {{action showLogin}} class='btn btn-primary btn-small'>{{i18n log_in}}</button>
|
||||
{{/if}}
|
||||
@ -85,7 +85,7 @@
|
||||
</li>
|
||||
<li class='current-user'>
|
||||
{{#if currentUser}}
|
||||
{{#linkTo 'userActivity.index' currentUser titleKey="current_user" class="icon"}}{{avatar currentUser imageSize="medium" }}{{/linkTo}}
|
||||
{{#linkTo 'userActivity.index' currentUser titleKey="current_user" class="icon"}}{{boundAvatar currentUser imageSize="medium" }}{{/linkTo}}
|
||||
{{else}}
|
||||
<div class="icon not-logged-in-avatar" {{action showLogin}}><i class='icon-user'></i></div>
|
||||
{{/if}}
|
||||
|
@ -0,0 +1,39 @@
|
||||
<form class="form-horizontal">
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<h3>{{i18n user.change_avatar.title}}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n user.avatar.title}}</label>
|
||||
<div class="controls">
|
||||
<label class="radio">
|
||||
<input type="radio" name="avatar" value="gravatar" {{action toggleUseUploadedAvatar false}}> {{avatar this imageSize="large" template="gravatar_template"}} {{i18n user.change_avatar.gravatar}} <a href="//gravatar.com/emails/" target="_blank" class="btn pad-left" title="{{i18n user.change_avatar.gravatar_title}}">{{i18n user.change}}</a>
|
||||
</label>
|
||||
{{#if has_uploaded_avatar}}
|
||||
<label class="radio">
|
||||
<input type="radio" name="avatar" value="uploaded_avatar" {{action toggleUseUploadedAvatar true}}> {{boundUploadedAvatar this imageSize="large"}} {{i18n user.change_avatar.uploaded_avatar}}
|
||||
</label>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="instructions">{{i18n user.change_avatar.upload_instructions}}</div>
|
||||
<div class="controls">
|
||||
<div>
|
||||
<input type="file" id="avatar-input" accept="image/*">
|
||||
</div>
|
||||
<button {{action uploadAvatar}} {{bindAttr disabled="uploadDisabled"}} class="btn btn-primary">
|
||||
<span class="add-upload"><i class="icon-picture"></i><i class="icon-plus"></i></span>
|
||||
{{uploadButtonText}}
|
||||
</button>
|
||||
{{#if uploading}}
|
||||
<span>{{i18n upload_selector.uploading}} {{uploadProgress}}%</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
@ -47,7 +47,17 @@
|
||||
{{avatar model imageSize="large"}}
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{{i18n user.avatar.instructions}}} {{email}}
|
||||
{{#if Discourse.SiteSettings.allow_uploaded_avatars}}
|
||||
{{#if use_uploaded_avatar}}
|
||||
{{{i18n user.avatar.instructions.uploaded_avatar}}}
|
||||
{{else}}
|
||||
{{{i18n user.avatar.instructions.gravatar}}} {{email}}
|
||||
{{/if}}
|
||||
{{#linkTo "preferences.avatar" class="btn pad-left"}}{{i18n user.change}}{{/linkTo}}
|
||||
{{else}}
|
||||
{{{i18n user.avatar.instructions.gravatar}}} {{email}}
|
||||
<a href="//gravatar.com/emails/" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn pad-left">{{i18n user.change}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -29,7 +29,7 @@
|
||||
{{/if}}
|
||||
</ul>
|
||||
<div class='avatar-wrapper'>
|
||||
{{boundAvatar model imageSize="120"}}
|
||||
{{boundAvatar model imageSize="huge"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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')
|
||||
});
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
|
@ -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')
|
||||
|
||||
});
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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+/}
|
||||
|
7
db/migrate/20130809211409_add_avatar_to_users.rb
Normal file
7
db/migrate/20130809211409_add_avatar_to_users.rb
Normal file
@ -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
|
@ -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
|
||||
# <rails>/public/uploads/site/_optimized/123/456/<filename>
|
||||
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/<site>/_optimized/<1A3>/<B5C>/<filename>
|
||||
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
|
||||
|
@ -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}"
|
||||
# <id><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 = [
|
||||
# <id><sha1>_<width>x<height><extension>
|
||||
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/<sha1>/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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -135,7 +135,7 @@ describe CookedPostProcessor do
|
||||
|
||||
it "generates overlay information" do
|
||||
cpp.post_process_images
|
||||
cpp.html.should match_html '<div><a href="http://test.localhost/uploads/default/1/1234567890123456.jpg" class="lightbox"><img src="http://test.localhost/uploads/default/_optimized/da3/9a3/ee5e6b4b0d3_100x200.jpg" width="690" height="1380"><div class="meta">
|
||||
cpp.html.should match_html '<div><a href="http://test.localhost/uploads/default/1/1234567890123456.jpg" class="lightbox"><img src="http://test.localhost/uploads/default/_optimized/da3/9a3/ee5e6b4b0d_100x200.jpg" width="690" height="1380"><div class="meta">
|
||||
<span class="filename">uploaded.jpg</span><span class="informations">1000x2000 1.21 KB</span><span class="expand"></span>
|
||||
</div></a></div>'
|
||||
cpp.should be_dirty
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
|
||||
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 "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
|
||||
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 "<p></p><aside class=\"quote\" data-post=\"555\" data-topic=\"666\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"/users/eviltrout/avatar/40?__ws=http%3A%2F%2Ftest.localhost\" class=\"avatar\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
|
||||
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 "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
|
||||
end
|
||||
|
||||
it "should produce a quote" do
|
||||
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
|
||||
end
|
||||
|
||||
it "trims spaces on quote params" do
|
||||
PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]").should match_html "<p></p><aside class=\"quote\" data-post=\"555\" data-topic=\"666\"><div class=\"title\">\n <div class=\"quote-controls\"></div>\n <img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">\n EvilTrout said:\n </div>\n <blockquote>ddd</blockquote>\n</aside><p></p>"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
it "should handle 3 mentions in a row" do
|
||||
PrettyText.cook('@hello @hello @hello').should match_html "<p><span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span> <span class=\"mention\">@hello</span></p>"
|
||||
|
@ -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
|
||||
|
@ -2,7 +2,6 @@
|
||||
module("Discourse.BBCode");
|
||||
|
||||
var format = function(input, expected, text) {
|
||||
|
||||
var cooked = Discourse.Markdown.cook(input, {lookupAvatar: false});
|
||||
equal(cooked, "<p>" + expected + "</p>", text);
|
||||
};
|
||||
|
@ -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'}),
|
||||
"<img width='20' height='20' src='/users/eviltrout/avatar/20?__ws=' class='avatar'>",
|
||||
var avatarTemplate = "/path/to/avatar/{size}.png";
|
||||
equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}),
|
||||
"<img width='20' height='20' src='/path/to/avatar/20.png' class='avatar'>",
|
||||
"it returns the avatar html");
|
||||
|
||||
equal(Discourse.Utilities.avatarImg({username: 'eviltrout', size: 'tiny', title: 'evilest trout'}),
|
||||
"<img width='20' height='20' src='/users/eviltrout/avatar/20?__ws=' class='avatar' title='evilest trout'>",
|
||||
equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}),
|
||||
"<img width='20' height='20' src='/path/to/avatar/20.png' class='avatar' title='evilest trout'>",
|
||||
"it adds a title if supplied");
|
||||
|
||||
equal(Discourse.Utilities.avatarImg({username: 'eviltrout', size: 'tiny', extraClasses: 'evil fish'}),
|
||||
"<img width='20' height='20' src='/users/eviltrout/avatar/20?__ws=' class='avatar evil fish'>",
|
||||
equal(Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}),
|
||||
"<img width='20' height='20' src='/path/to/avatar/20.png' class='avatar 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");
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user