mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 15:48:43 -05:00
custom avatar support
This commit is contained in:
parent
e5e3164ea1
commit
c867b67a0b
34 changed files with 576 additions and 201 deletions
|
@ -252,12 +252,22 @@ Discourse.BBCode = {
|
||||||
// remove leading <br>s
|
// remove leading <br>s
|
||||||
var content = matches[2].trim();
|
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
|
// Arguments for formatting
|
||||||
args = {
|
args = {
|
||||||
username: I18n.t('user.said',{username: username}),
|
username: I18n.t('user.said', {username: username}),
|
||||||
params: params,
|
params: params,
|
||||||
quote: content,
|
quote: content,
|
||||||
avatarImg: opts.lookupAvatar ? opts.lookupAvatar(username) : void 0
|
avatarImg: avatarImg
|
||||||
};
|
};
|
||||||
|
|
||||||
// Name of the template
|
// Name of the template
|
||||||
|
|
|
@ -16,6 +16,7 @@ Discourse.Utilities = {
|
||||||
case 'small': return 25;
|
case 'small': return 25;
|
||||||
case 'medium': return 32;
|
case 'medium': return 32;
|
||||||
case 'large': return 45;
|
case 'large': return 45;
|
||||||
|
case 'huge': return 120;
|
||||||
}
|
}
|
||||||
return size;
|
return size;
|
||||||
},
|
},
|
||||||
|
@ -50,18 +51,20 @@ Discourse.Utilities = {
|
||||||
return result + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "</a>";
|
return result + "style=\"background-color: #" + color + "; color: #" + textColor + ";\">" + name + "</a>";
|
||||||
},
|
},
|
||||||
|
|
||||||
avatarUrl: function(username, size, template) {
|
avatarUrl: function(template, size) {
|
||||||
if (!username) return "";
|
if (!template) { return ""; }
|
||||||
var rawSize = (Discourse.Utilities.translateSize(size) * (window.devicePixelRatio || 1)).toFixed();
|
var rawSize = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize(size));
|
||||||
|
return template.replace(/\{size\}/g, rawSize);
|
||||||
|
},
|
||||||
|
|
||||||
if (username.match(/[^A-Za-z0-9_]/)) { return ""; }
|
getRawSize: function(size) {
|
||||||
if (template) return template.replace(/\{size\}/g, rawSize);
|
var pixelRatio = window.devicePixelRatio || 1;
|
||||||
return Discourse.getURL("/users/") + username.toLowerCase() + "/avatar/" + rawSize + "?__ws=" + encodeURIComponent(Discourse.BaseUrl || "");
|
return pixelRatio >= 1.5 ? size * 2 : size;
|
||||||
},
|
},
|
||||||
|
|
||||||
avatarImg: function(options) {
|
avatarImg: function(options) {
|
||||||
var size = Discourse.Utilities.translateSize(options.size);
|
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
|
// We won't render an invalid url
|
||||||
if (!url || url.length === 0) { return ""; }
|
if (!url || url.length === 0) { return ""; }
|
||||||
|
@ -71,8 +74,8 @@ Discourse.Utilities = {
|
||||||
return "<img width='" + size + "' height='" + size + "' src='" + url + "' class='" + classes + "'" + title + ">";
|
return "<img width='" + size + "' height='" + size + "' src='" + url + "' class='" + classes + "'" + title + ">";
|
||||||
},
|
},
|
||||||
|
|
||||||
tinyAvatar: function(username) {
|
tinyAvatar: function(avatarTemplate) {
|
||||||
return Discourse.Utilities.avatarImg({ username: username, size: 'tiny' });
|
return Discourse.Utilities.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny' });
|
||||||
},
|
},
|
||||||
|
|
||||||
postUrl: function(slug, topicId, postNumber) {
|
postUrl: function(slug, topicId, postNumber) {
|
||||||
|
@ -266,6 +269,28 @@ Discourse.Utilities = {
|
||||||
|
|
||||||
authorizedExtensions: function() {
|
authorizedExtensions: function() {
|
||||||
return Discourse.SiteSettings.authorized_extensions.replace(/\|/g, ", ");
|
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
|
@for Handlebars
|
||||||
**/
|
**/
|
||||||
Handlebars.registerHelper('avatar', function(user, options) {
|
Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
|
|
||||||
if (typeof user === 'string') {
|
if (typeof user === 'string') {
|
||||||
user = Ember.Handlebars.get(this, user, options);
|
user = Ember.Handlebars.get(this, user, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
if( user ) {
|
if (user) {
|
||||||
var username = Em.get(user, 'username');
|
var username = Em.get(user, 'username');
|
||||||
if (!username) username = Em.get(user, options.hash.usernamePath);
|
if (!username) username = Em.get(user, options.hash.usernamePath);
|
||||||
|
|
||||||
var avatarTemplate = 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');
|
if (!avatarTemplate) avatarTemplate = Em.get(user, 'user.avatar_template');
|
||||||
|
|
||||||
var title;
|
var title;
|
||||||
|
@ -147,7 +153,6 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
||||||
size: options.hash.imageSize,
|
size: options.hash.imageSize,
|
||||||
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
|
extraClasses: Em.get(user, 'extras') || options.hash.extraClasses,
|
||||||
username: username,
|
|
||||||
title: title || username,
|
title: title || username,
|
||||||
avatarTemplate: avatarTemplate
|
avatarTemplate: avatarTemplate
|
||||||
}));
|
}));
|
||||||
|
@ -158,18 +163,32 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Bound avatar helper.
|
Bound avatar helper.
|
||||||
|
Will rerender whenever the "avatar_template" changes.
|
||||||
|
|
||||||
@method boundAvatar
|
@method boundAvatar
|
||||||
@for Handlebars
|
@for Handlebars
|
||||||
**/
|
**/
|
||||||
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
|
Ember.Handlebars.registerBoundHelper('boundAvatar', function(user, options) {
|
||||||
var username = Em.get(user, 'username');
|
|
||||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
||||||
size: options.hash.imageSize,
|
size: options.hash.imageSize,
|
||||||
username: username,
|
avatarTemplate: Em.get(user, 'avatar_template')
|
||||||
avatarTemplate: Ember.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.
|
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) {
|
if (post) {
|
||||||
postDescription = I18n.t('post.' + this.get('action'), {
|
postDescription = I18n.t('post.' + this.get('action'), {
|
||||||
link: postLink,
|
link: postLink,
|
||||||
replyAvatar: Discourse.Utilities.tinyAvatar(post.get('username')),
|
replyAvatar: Discourse.Utilities.tinyAvatar(post.get('avatar_template')),
|
||||||
username: this.get('post.username')
|
username: this.get('post.username')
|
||||||
});
|
});
|
||||||
|
|
||||||
var replyUsername = post.get('reply_to_user.username');
|
var replyUsername = post.get('reply_to_user.username');
|
||||||
if (replyUsername && this.get('action') === EDIT) {
|
var replyAvatarTemplate = post.get('reply_to_user.avatar_template');
|
||||||
postDescription += " " + I18n.t("post.in_reply_to") + " " +
|
if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) {
|
||||||
Discourse.Utilities.tinyAvatar(replyUsername) + " " + replyUsername;
|
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('username', { path: '/username' });
|
||||||
this.route('email', { path: '/email' });
|
this.route('email', { path: '/email' });
|
||||||
this.route('about', { path: '/about-me' });
|
this.route('about', { path: '/about-me' });
|
||||||
|
this.route('avatar', { path: '/avatar' });
|
||||||
});
|
});
|
||||||
|
|
||||||
this.route('invited', { path: 'invited' });
|
this.route('invited', { path: 'invited' });
|
||||||
|
|
|
@ -116,4 +116,33 @@ Discourse.PreferencesUsernameRoute = Discourse.RestrictedUserRoute.extend({
|
||||||
setupController: function(controller, user) {
|
setupController: function(controller, user) {
|
||||||
controller.setProperties({ model: user, newUsername: user.get('username') });
|
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}}
|
{{#unless showExtraInfo}}
|
||||||
<div class='current-username'>
|
<div class='current-username'>
|
||||||
{{#if currentUser}}
|
{{#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}}
|
{{else}}
|
||||||
<button {{action showLogin}} class='btn btn-primary btn-small'>{{i18n log_in}}</button>
|
<button {{action showLogin}} class='btn btn-primary btn-small'>{{i18n log_in}}</button>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li class='current-user'>
|
<li class='current-user'>
|
||||||
{{#if currentUser}}
|
{{#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}}
|
{{else}}
|
||||||
<div class="icon not-logged-in-avatar" {{action showLogin}}><i class='icon-user'></i></div>
|
<div class="icon not-logged-in-avatar" {{action showLogin}}><i class='icon-user'></i></div>
|
||||||
{{/if}}
|
{{/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"}}
|
{{avatar model imageSize="large"}}
|
||||||
</div>
|
</div>
|
||||||
<div class='instructions'>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
<div class='avatar-wrapper'>
|
<div class='avatar-wrapper'>
|
||||||
{{boundAvatar model imageSize="120"}}
|
{{boundAvatar model imageSize="huge"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,7 +38,6 @@ Discourse.ActionsHistoryView = Discourse.View.extend({
|
||||||
}
|
}
|
||||||
iconsHtml += Discourse.Utilities.avatarImg({
|
iconsHtml += Discourse.Utilities.avatarImg({
|
||||||
size: 'small',
|
size: 'small',
|
||||||
username: u.get('username'),
|
|
||||||
avatarTemplate: u.get('avatar_template'),
|
avatarTemplate: u.get('avatar_template'),
|
||||||
title: u.get('username')
|
title: u.get('username')
|
||||||
});
|
});
|
||||||
|
|
|
@ -191,8 +191,11 @@ Discourse.ComposerView = Discourse.View.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editor = editor = Discourse.Markdown.createEditor({
|
this.editor = editor = Discourse.Markdown.createEditor({
|
||||||
lookupAvatar: function(username) {
|
lookupAvatarByPostNumber: function(postNumber) {
|
||||||
return Discourse.Utilities.avatarImg({ username: username, size: 'tiny' });
|
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) {
|
$uploadTarget.on('fileuploadfail', function (e, data) {
|
||||||
// hide upload status
|
// hide upload status
|
||||||
composerView.set('isUploading', false);
|
composerView.set('isUploading', false);
|
||||||
// deal with meaningful errors first
|
// display an error message
|
||||||
if (data.jqXHR) {
|
Discourse.Utilities.displayErrorForUpload(data);
|
||||||
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'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// I hate to use Em.run.later, but I don't think there's a way of waiting for a CSS transition
|
// 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() {
|
return Em.run.later(jQuery, (function() {
|
||||||
var replyTitle = $('#reply-title');
|
var replyTitle = $('#reply-title');
|
||||||
composerView.resize();
|
composerView.resize();
|
||||||
if (replyTitle.length) {
|
return replyTitle.length ? replyTitle.putCursorAtEnd() : $wmdInput.putCursorAtEnd();
|
||||||
return replyTitle.putCursorAtEnd();
|
|
||||||
} else {
|
|
||||||
return $wmdInput.putCursorAtEnd();
|
|
||||||
}
|
|
||||||
}), 300);
|
}), 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
|
class TopicsController < ApplicationController
|
||||||
|
|
||||||
# Avatar is an image request, not XHR
|
|
||||||
before_filter :ensure_logged_in, only: [:timings,
|
before_filter :ensure_logged_in, only: [:timings,
|
||||||
:destroy_timings,
|
:destroy_timings,
|
||||||
:update,
|
:update,
|
||||||
|
@ -22,8 +21,7 @@ class TopicsController < ApplicationController
|
||||||
|
|
||||||
before_filter :consider_user_for_promotion, only: :show
|
before_filter :consider_user_for_promotion, only: :show
|
||||||
|
|
||||||
skip_before_filter :check_xhr, only: [:avatar, :show, :feed]
|
skip_before_filter :check_xhr, only: [:show, :feed]
|
||||||
caches_action :avatar, cache_path: Proc.new {|c| "#{c.params[:post_number]}-#{c.params[:topic_id]}" }
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,7 @@ require_dependency 'user_name_suggester'
|
||||||
|
|
||||||
class UsersController < ApplicationController
|
class UsersController < ApplicationController
|
||||||
|
|
||||||
skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :activate_account, :avatar, :authorize_email, :user_preferences_redirect]
|
skip_before_filter :check_xhr, only: [:show, :password_reset, :update, :activate_account, :authorize_email, :user_preferences_redirect]
|
||||||
skip_before_filter :authorize_mini_profiler, only: [:avatar]
|
|
||||||
|
|
||||||
before_filter :ensure_logged_in, only: [:username, :update, :change_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}
|
render json: {value: honeypot_value, challenge: challenge_value}
|
||||||
end
|
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
|
def password_reset
|
||||||
expires_now()
|
expires_now()
|
||||||
|
|
||||||
|
@ -336,6 +316,46 @@ class UsersController < ApplicationController
|
||||||
methods: :avatar_template) }
|
methods: :avatar_template) }
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def honeypot_value
|
def honeypot_value
|
||||||
|
@ -405,12 +425,4 @@ class UsersController < ApplicationController
|
||||||
auth[:github_user_id] && auth[:github_screen_name] &&
|
auth[:github_user_id] && auth[:github_screen_name] &&
|
||||||
GithubUserInfo.find_by_github_user_id(auth[:github_user_id]).nil?
|
GithubUserInfo.find_by_github_user_id(auth[:github_user_id]).nil?
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -31,11 +31,15 @@ class Upload < ActiveRecord::Base
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
Upload.transaction do
|
Upload.transaction do
|
||||||
Discourse.store.remove_file(url)
|
Discourse.store.remove_upload(self)
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def extension
|
||||||
|
File.extname(original_filename)
|
||||||
|
end
|
||||||
|
|
||||||
def self.create_for(user_id, file, filesize)
|
def self.create_for(user_id, file, filesize)
|
||||||
# compute the sha
|
# compute the sha
|
||||||
sha1 = Digest::SHA1.file(file.tempfile).hexdigest
|
sha1 = Digest::SHA1.file(file.tempfile).hexdigest
|
||||||
|
|
|
@ -28,6 +28,7 @@ class User < ActiveRecord::Base
|
||||||
has_many :user_visits
|
has_many :user_visits
|
||||||
has_many :invites
|
has_many :invites
|
||||||
has_many :topic_links
|
has_many :topic_links
|
||||||
|
has_many :uploads
|
||||||
|
|
||||||
has_one :facebook_user_info, dependent: :destroy
|
has_one :facebook_user_info, dependent: :destroy
|
||||||
has_one :twitter_user_info, dependent: :destroy
|
has_one :twitter_user_info, dependent: :destroy
|
||||||
|
@ -41,6 +42,8 @@ class User < ActiveRecord::Base
|
||||||
|
|
||||||
has_one :user_search_data
|
has_one :user_search_data
|
||||||
|
|
||||||
|
belongs_to :uploaded_avatar, class_name: 'Upload', dependent: :destroy
|
||||||
|
|
||||||
validates_presence_of :username
|
validates_presence_of :username
|
||||||
validate :username_validator
|
validate :username_validator
|
||||||
validates :email, presence: true, uniqueness: true
|
validates :email, presence: true, uniqueness: true
|
||||||
|
@ -295,24 +298,38 @@ class User < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.avatar_template(email)
|
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)
|
email_hash = self.email_hash(email)
|
||||||
# robohash was possibly causing caching issues
|
"//www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon"
|
||||||
# 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"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Don't pass this up to the client - it's meant for server side use
|
# Don't pass this up to the client - it's meant for server side use
|
||||||
# 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
|
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
|
end
|
||||||
|
|
||||||
# return null for local avatars, a template for gravatar
|
|
||||||
def avatar_template
|
def avatar_template
|
||||||
User.avatar_template(email)
|
User.avatar_template(email)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Updates the denormalized view counts for all users
|
# Updates the denormalized view counts for all users
|
||||||
def self.update_view_counts
|
def self.update_view_counts
|
||||||
# Update denormalized topics_entered
|
# Update denormalized topics_entered
|
||||||
|
@ -506,6 +523,9 @@ class User < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_uploaded_avatar
|
||||||
|
uploaded_avatar.present?
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
@ -529,7 +549,6 @@ class User < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def create_email_token
|
def create_email_token
|
||||||
email_tokens.create(email: email)
|
email_tokens.create(email: email)
|
||||||
end
|
end
|
||||||
|
@ -571,13 +590,13 @@ class User < ActiveRecord::Base
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def send_approval_email
|
def send_approval_email
|
||||||
Jobs.enqueue(:user_email,
|
Jobs.enqueue(:user_email,
|
||||||
type: :signup_after_approval,
|
type: :signup_after_approval,
|
||||||
user_id: id,
|
user_id: id,
|
||||||
email_token: email_tokens.first.token
|
email_token: email_tokens.first.token
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
@ -647,6 +666,9 @@ end
|
||||||
# blocked :boolean default(FALSE)
|
# blocked :boolean default(FALSE)
|
||||||
# dynamic_favicon :boolean default(FALSE), not null
|
# dynamic_favicon :boolean default(FALSE), not null
|
||||||
# title :string(255)
|
# title :string(255)
|
||||||
|
# use_uploaded_avatar :boolean default(FALSE)
|
||||||
|
# uploaded_avatar_template :string(255)
|
||||||
|
# uploaded_avatar_id :integer
|
||||||
#
|
#
|
||||||
# Indexes
|
# Indexes
|
||||||
#
|
#
|
||||||
|
|
|
@ -14,7 +14,11 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
:external_links_in_new_tab,
|
:external_links_in_new_tab,
|
||||||
:dynamic_favicon,
|
:dynamic_favicon,
|
||||||
:trust_level,
|
:trust_level,
|
||||||
:can_edit
|
:can_edit,
|
||||||
|
:use_uploaded_avatar,
|
||||||
|
:has_uploaded_avatar,
|
||||||
|
:gravatar_template,
|
||||||
|
:uploaded_avatar_template
|
||||||
|
|
||||||
def include_site_flagged_posts_count?
|
def include_site_flagged_posts_count?
|
||||||
object.staff?
|
object.staff?
|
||||||
|
@ -36,4 +40,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def gravatar_template
|
||||||
|
User.gravatar_template(object.email)
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,7 +111,8 @@ class PostSerializer < BasicPostSerializer
|
||||||
def reply_to_user
|
def reply_to_user
|
||||||
{
|
{
|
||||||
username: object.reply_to_user.username,
|
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
|
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/about-me' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username/preferences/username' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}
|
get 'users/:username/preferences/username' => 'users#preferences', constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||||
put 'users/:username/preferences/username' => 'users#username', constraints: {username: USERNAME_ROUTE_FORMAT}
|
put 'users/:username/preferences/username' => 'users#username', constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||||
get 'users/:username/avatar(/:size)' => 'users#avatar', constraints: {username: USERNAME_ROUTE_FORMAT}
|
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}
|
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}
|
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}
|
get 'users/:username/activity' => 'users#show', constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||||
|
@ -143,7 +145,6 @@ Discourse::Application.routes.draw do
|
||||||
|
|
||||||
resources :uploads
|
resources :uploads
|
||||||
|
|
||||||
|
|
||||||
get 'posts/by_number/:topic_id/:post_number' => 'posts#by_number'
|
get 'posts/by_number/:topic_id/:post_number' => 'posts#by_number'
|
||||||
get 'posts/:id/reply-history' => 'posts#reply_history'
|
get 'posts/:id/reply-history' => 'posts#reply_history'
|
||||||
resources :posts do
|
resources :posts do
|
||||||
|
@ -211,9 +212,6 @@ Discourse::Application.routes.draw do
|
||||||
get 'topics/similar_to'
|
get 'topics/similar_to'
|
||||||
get 'topics/created-by/:username' => 'list#topics_by', as: 'topics_by', constraints: {username: USERNAME_ROUTE_FORMAT}
|
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
|
# Topic routes
|
||||||
get 't/:slug/:topic_id/wordpress' => 'topics#wordpress', constraints: {topic_id: /\d+/}
|
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+/}
|
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
|
class LocalStore
|
||||||
|
|
||||||
def store_upload(file, upload)
|
def store_upload(file, upload)
|
||||||
unique_sha1 = Digest::SHA1.hexdigest("#{Time.now.to_s}#{file.original_filename}")[0,16]
|
path = get_path_for_upload(file, upload)
|
||||||
extension = File.extname(file.original_filename)
|
store_file(file, path)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def store_optimized_image(file, optimized_image)
|
def store_optimized_image(file, optimized_image)
|
||||||
# 1234567890ABCDEF_100x200.jpg
|
path = get_path_for_optimized_image(file, optimized_image)
|
||||||
filename = [
|
store_file(file, path)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_file(url)
|
def store_avatar(file, upload, size)
|
||||||
File.delete("#{public_dir}#{url}") if has_been_uploaded?(url)
|
path = get_path_for_avatar(file, upload, size)
|
||||||
rescue Errno::ENOENT
|
store_file(file, path)
|
||||||
# don't care if the file isn't there
|
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
|
end
|
||||||
|
|
||||||
def has_been_uploaded?(url)
|
def has_been_uploaded?(url)
|
||||||
|
@ -63,8 +53,63 @@ class LocalStore
|
||||||
"#{public_dir}#{upload.url}"
|
"#{public_dir}#{upload.url}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def absolute_avatar_template(upload)
|
||||||
|
avatar_template(upload, absolute_base_url)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
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)
|
def copy_file(file, path)
|
||||||
FileUtils.mkdir_p Pathname.new(path).dirname
|
FileUtils.mkdir_p Pathname.new(path).dirname
|
||||||
# move the file to the right location
|
# move the file to the right location
|
||||||
|
@ -74,6 +119,17 @@ class LocalStore
|
||||||
end
|
end
|
||||||
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)
|
def is_relative?(url)
|
||||||
url.start_with?(relative_base_url)
|
url.start_with?(relative_base_url)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,34 +4,60 @@ require 'open-uri'
|
||||||
class S3Store
|
class S3Store
|
||||||
|
|
||||||
def store_upload(file, upload)
|
def store_upload(file, upload)
|
||||||
extension = File.extname(file.original_filename)
|
# <id><sha1><extension>
|
||||||
remote_filename = "#{upload.id}#{upload.sha1}#{extension}"
|
path = "#{upload.id}#{upload.sha1}#{upload.extension}"
|
||||||
|
|
||||||
# if this fails, it will throw an exception
|
# 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
|
# returns the url of the uploaded file
|
||||||
"#{absolute_base_url}/#{remote_filename}"
|
"#{absolute_base_url}/#{path}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def store_optimized_image(file, optimized_image)
|
def store_optimized_image(file, optimized_image)
|
||||||
extension = File.extname(file.path)
|
# <id><sha1>_<width>x<height><extension>
|
||||||
remote_filename = [
|
path = [
|
||||||
optimized_image.id,
|
optimized_image.id,
|
||||||
optimized_image.sha1,
|
optimized_image.sha1,
|
||||||
"_#{optimized_image.width}x#{optimized_image.height}",
|
"_#{optimized_image.width}x#{optimized_image.height}",
|
||||||
extension
|
optimized_image.extension
|
||||||
].join
|
].join
|
||||||
|
|
||||||
# if this fails, it will throw an exception
|
# if this fails, it will throw an exception
|
||||||
upload(file, remote_filename)
|
upload(file, path)
|
||||||
|
|
||||||
# returns the url of the uploaded file
|
# 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
|
end
|
||||||
|
|
||||||
def remove_file(url)
|
def remove_file(url)
|
||||||
check_missing_site_settings
|
|
||||||
return unless has_been_uploaded?(url)
|
return unless has_been_uploaded?(url)
|
||||||
name = File.basename(url)
|
name = File.basename(url)
|
||||||
remove(name)
|
remove(name)
|
||||||
|
|
|
@ -23,7 +23,7 @@ module Oneboxer
|
||||||
|
|
||||||
return @url unless Guardian.new.can_see?(user)
|
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?
|
args[:bio] = user.bio_cooked if user.bio_cooked.present?
|
||||||
|
|
||||||
@template = 'user'
|
@template = 'user'
|
||||||
|
@ -58,7 +58,7 @@ module Oneboxer
|
||||||
|
|
||||||
posters = topic.posters_summary.map do |p|
|
posters = topic.posters_summary.map do |p|
|
||||||
{username: p[:user][:username],
|
{username: p[:user][:username],
|
||||||
avatar: PrettyText.avatar_img(p[:user][:username], 'tiny'),
|
avatar: PrettyText.avatar_img(p[:user][:avatar_template], 'tiny'),
|
||||||
description: p[:description],
|
description: p[:description],
|
||||||
extras: p[:extras]}
|
extras: p[:extras]}
|
||||||
end
|
end
|
||||||
|
|
|
@ -64,7 +64,7 @@ module PrettyText
|
||||||
return "" unless username
|
return "" unless username
|
||||||
|
|
||||||
user = User.where(username_lower: username.downcase).first
|
user = User.where(username_lower: username.downcase).first
|
||||||
if user
|
if user.present?
|
||||||
user.avatar_template
|
user.avatar_template
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -139,7 +139,7 @@ module PrettyText
|
||||||
v8['opts'] = opts || {}
|
v8['opts'] = opts || {}
|
||||||
v8['raw'] = text
|
v8['raw'] = text
|
||||||
v8.eval('opts["mentionLookup"] = function(u){return helpers.is_username_valid(u);}')
|
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)')
|
baked = v8.eval('Discourse.Markdown.markdownConverter(opts).makeHtml(raw)')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -149,15 +149,15 @@ module PrettyText
|
||||||
end
|
end
|
||||||
|
|
||||||
# leaving this here, cause it invokes v8, don't want to implement twice
|
# 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
|
r = nil
|
||||||
@mutex.synchronize do
|
@mutex.synchronize do
|
||||||
v8['username'] = username
|
v8['avatarTemplate'] = avatar_template
|
||||||
v8['size'] = size
|
v8['size'] = size
|
||||||
v8.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
|
v8.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
|
||||||
v8.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
|
v8.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
|
||||||
v8.eval("Discourse.BaseUrl = '#{RailsMultisite::ConnectionManagement.current_hostname}';")
|
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
|
end
|
||||||
r
|
r
|
||||||
end
|
end
|
||||||
|
|
|
@ -135,7 +135,7 @@ describe CookedPostProcessor do
|
||||||
|
|
||||||
it "generates overlay information" do
|
it "generates overlay information" do
|
||||||
cpp.post_process_images
|
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>
|
<span class="filename">uploaded.jpg</span><span class="informations">1000x2000 1.21 KB</span><span class="expand"></span>
|
||||||
</div></a></div>'
|
</div></a></div>'
|
||||||
cpp.should be_dirty
|
cpp.should be_dirty
|
||||||
|
|
|
@ -35,25 +35,38 @@ describe LocalStore do
|
||||||
|
|
||||||
it "returns a relative url" do
|
it "returns a relative url" do
|
||||||
store.expects(:copy_file)
|
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
|
||||||
|
|
||||||
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
|
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
|
end
|
||||||
|
|
||||||
it "deletes the file locally" do
|
it "deletes the file locally" do
|
||||||
File.expects(:delete)
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "remove_optimized_image" do
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "remove_avatar" do
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
describe "has_been_uploaded?" do
|
describe "has_been_uploaded?" do
|
||||||
|
|
||||||
it "identifies local or relatives urls" do
|
it "identifies local or relatives urls" do
|
||||||
|
|
|
@ -40,6 +40,7 @@ describe S3Store do
|
||||||
|
|
||||||
it "returns a relative url" do
|
it "returns a relative url" do
|
||||||
upload.stubs(:id).returns(42)
|
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"
|
store.store_upload(uploaded_file, upload).should == "//s3_upload_bucket.s3.amazonaws.com/42e9d71f5ee7c92d6dc9e92ffdad17b8bd49418f98.png"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,20 +55,32 @@ describe S3Store do
|
||||||
|
|
||||||
end
|
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.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
|
end
|
||||||
|
|
||||||
it "deletes the file on s3" do
|
it "deletes the file on s3" do
|
||||||
store.expects(:remove)
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "remove_optimized_image" do
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "remove_avatar" do
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
describe "has_been_uploaded?" do
|
describe "has_been_uploaded?" do
|
||||||
|
|
||||||
it "identifies S3 uploads" do
|
it "identifies S3 uploads" do
|
||||||
|
|
|
@ -628,7 +628,7 @@ describe Guardian do
|
||||||
Guardian.new(nil).can_see_flags?(post).should be_false
|
Guardian.new(nil).can_see_flags?(post).should be_false
|
||||||
end
|
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
|
Guardian.new(user).can_see_flags?(post).should be_false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -14,18 +14,27 @@ test
|
||||||
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]").should =~ /\[sam\]/
|
PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"][sam][/quote]").should =~ /\[sam\]/
|
||||||
end
|
end
|
||||||
|
|
||||||
it "produces a quote even with new lines in it" do
|
describe "with avatar" 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
|
|
||||||
|
|
||||||
it "should produce a quote" do
|
before(:each) 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>"
|
eviltrout = User.new
|
||||||
end
|
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
|
it "produces a quote even with new lines in it" 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>"
|
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
|
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
|
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>"
|
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.extension.should == ".jpg"
|
||||||
oi.width.should == 100
|
oi.width.should == 100
|
||||||
oi.height.should == 200
|
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
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
module("Discourse.BBCode");
|
module("Discourse.BBCode");
|
||||||
|
|
||||||
var format = function(input, expected, text) {
|
var format = function(input, expected, text) {
|
||||||
|
|
||||||
var cooked = Discourse.Markdown.cook(input, {lookupAvatar: false});
|
var cooked = Discourse.Markdown.cook(input, {lookupAvatar: false});
|
||||||
equal(cooked, "<p>" + expected + "</p>", text);
|
equal(cooked, "<p>" + expected + "</p>", text);
|
||||||
};
|
};
|
||||||
|
|
|
@ -110,33 +110,25 @@ test("isAnImage", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("avatarUrl", function() {
|
test("avatarUrl", function() {
|
||||||
blank(Discourse.Utilities.avatarUrl('', 'tiny'), "no avatar url returns blank");
|
blank(Discourse.Utilities.avatarUrl('', 'tiny'), "no template returns blank");
|
||||||
blank(Discourse.Utilities.avatarUrl('this is not a username', 'tiny'), "invalid username 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");
|
||||||
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");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("avatarImg", function() {
|
test("avatarImg", function() {
|
||||||
equal(Discourse.Utilities.avatarImg({username: 'eviltrout', size: 'tiny'}),
|
var avatarTemplate = "/path/to/avatar/{size}.png";
|
||||||
"<img width='20' height='20' src='/users/eviltrout/avatar/20?__ws=' class='avatar'>",
|
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");
|
"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'}),
|
||||||
"<img width='20' height='20' src='/users/eviltrout/avatar/20?__ws=' class='avatar' 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");
|
"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'}),
|
||||||
"<img width='20' height='20' src='/users/eviltrout/avatar/20?__ws=' class='avatar evil fish'>",
|
"<img width='20' height='20' src='/path/to/avatar/20.png' class='avatar evil fish'>",
|
||||||
"it adds extra classes if supplied");
|
"it adds extra classes if supplied");
|
||||||
|
|
||||||
blank(Discourse.Utilities.avatarImg({username: 'weird*username', size: 'tiny'}),
|
blank(Discourse.Utilities.avatarImg({avatarTemplate: "", size: 'tiny'}),
|
||||||
"it doesn't render avatars for invalid usernames");
|
"it doesn't render avatars for invalid avatar template");
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue