Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
328f9a4a39
|
@ -2057,7 +2057,13 @@ var html = (function(html4) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discourse modification: give us more flexibility with whitelists
|
// Discourse modification: give us more flexibility with whitelists
|
||||||
if (opt_nmTokenPolicy && opt_nmTokenPolicy(tagName, attribName, value)) { continue; }
|
if (opt_nmTokenPolicy) {
|
||||||
|
var newValue = opt_nmTokenPolicy(tagName, attribName, value);
|
||||||
|
if (newValue) {
|
||||||
|
attribs[i + 1] = newValue;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (atype !== null) {
|
if (atype !== null) {
|
||||||
switch (atype) {
|
switch (atype) {
|
||||||
|
|
|
@ -14,7 +14,8 @@ var PosterNameComponent = Em.Component.extend({
|
||||||
var name = post.get('name'),
|
var name = post.get('name'),
|
||||||
username = post.get('username'),
|
username = post.get('username'),
|
||||||
linkClass = 'username',
|
linkClass = 'username',
|
||||||
primaryGroupName = post.get('primary_group_name');
|
primaryGroupName = post.get('primary_group_name'),
|
||||||
|
url = post.get('usernameUrl');
|
||||||
|
|
||||||
if (post.get('staff')) { linkClass += ' staff'; }
|
if (post.get('staff')) { linkClass += ' staff'; }
|
||||||
if (post.get('admin')) { linkClass += ' admin'; }
|
if (post.get('admin')) { linkClass += ' admin'; }
|
||||||
|
@ -25,7 +26,7 @@ var PosterNameComponent = Em.Component.extend({
|
||||||
linkClass += ' ' + primaryGroupName;
|
linkClass += ' ' + primaryGroupName;
|
||||||
}
|
}
|
||||||
// Main link
|
// Main link
|
||||||
buffer.push("<span class='" + linkClass + "'><a href='#'>" + username + "</a>");
|
buffer.push("<span class='" + linkClass + "'><a href='" + url + "' data-auto-route='true'>" + username + "</a>");
|
||||||
|
|
||||||
// Add a glyph if we have one
|
// Add a glyph if we have one
|
||||||
var glyph = this.posterGlyph(post);
|
var glyph = this.posterGlyph(post);
|
||||||
|
@ -37,7 +38,7 @@ var PosterNameComponent = Em.Component.extend({
|
||||||
// Are we showing full names?
|
// Are we showing full names?
|
||||||
if (name && this.get('displayNameOnPosts') && (this.sanitizeName(name) !== this.sanitizeName(username))) {
|
if (name && this.get('displayNameOnPosts') && (this.sanitizeName(name) !== this.sanitizeName(username))) {
|
||||||
name = Handlebars.Utils.escapeExpression(name);
|
name = Handlebars.Utils.escapeExpression(name);
|
||||||
buffer.push("<span class='full-name'><a href='#'>" + name + "</a></span>");
|
buffer.push("<span class='full-name'><a href='" + url + "' data-auto-route='true'>" + name + "</a></span>");
|
||||||
}
|
}
|
||||||
|
|
||||||
// User titles
|
// User titles
|
||||||
|
@ -60,9 +61,10 @@ var PosterNameComponent = Em.Component.extend({
|
||||||
|
|
||||||
click: function(e) {
|
click: function(e) {
|
||||||
var $target = $(e.target),
|
var $target = $(e.target),
|
||||||
href = $target.attr('href');
|
href = $target.attr('href'),
|
||||||
|
url = this.get('post.usernameUrl');
|
||||||
|
|
||||||
if (!Em.isEmpty(href) && href !== '#') {
|
if (!Em.isEmpty(href) && href !== url) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this.appEvents.trigger('poster:expand', $target);
|
this.appEvents.trigger('poster:expand', $target);
|
||||||
|
|
|
@ -34,7 +34,11 @@ export default Discourse.ObjectController.extend({
|
||||||
var currentUsername = this.get('username'),
|
var currentUsername = this.get('username'),
|
||||||
wasVisible = this.get('visible');
|
wasVisible = this.get('visible');
|
||||||
|
|
||||||
this.set('avatar', {username: username, uploaded_avatar_id: uploadedAvatarId});
|
if (uploadedAvatarId) {
|
||||||
|
this.set('avatar', {username: username, uploaded_avatar_id: uploadedAvatarId});
|
||||||
|
} else {
|
||||||
|
this.set('avatar', null);
|
||||||
|
}
|
||||||
|
|
||||||
this.setProperties({visible: true, username: username});
|
this.setProperties({visible: true, username: username});
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,8 @@ function daysSinceEpoch(dt) {
|
||||||
return dt.getTime() / 86400000;
|
return dt.getTime() / 86400000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var safe = Handlebars.SafeString;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Converts a date to a coldmap class
|
Converts a date to a coldmap class
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ function categoryLinkHTML(category, options) {
|
||||||
categoryOptions.categories = Em.Handlebars.get(this, options.hash.categories, options);
|
categoryOptions.categories = Em.Handlebars.get(this, options.hash.categories, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Handlebars.SafeString(Discourse.HTML.categoryBadge(category, categoryOptions));
|
return new safe(Discourse.HTML.categoryBadge(category, categoryOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -171,7 +173,7 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id');
|
var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id');
|
||||||
var avatarTemplate = Discourse.User.avatarTemplate(username,uploadedAvatarId);
|
var avatarTemplate = Discourse.User.avatarTemplate(username,uploadedAvatarId);
|
||||||
|
|
||||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
return new safe(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,
|
||||||
title: title || username,
|
title: title || username,
|
||||||
|
@ -189,7 +191,9 @@ Handlebars.registerHelper('avatar', function(user, options) {
|
||||||
@for Handlebars
|
@for Handlebars
|
||||||
**/
|
**/
|
||||||
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
|
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
|
||||||
if (Em.isEmpty(user)) { return; }
|
if (Em.isEmpty(user)) {
|
||||||
|
return new safe("<div class='avatar-placeholder'></div>");
|
||||||
|
}
|
||||||
var username = Em.get(user, 'username');
|
var username = Em.get(user, 'username');
|
||||||
|
|
||||||
if(arguments.length < 4){
|
if(arguments.length < 4){
|
||||||
|
@ -198,7 +202,7 @@ Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
|
||||||
|
|
||||||
var avatarTemplate = Discourse.User.avatarTemplate(username, uploadId);
|
var avatarTemplate = Discourse.User.avatarTemplate(username, uploadId);
|
||||||
|
|
||||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
return new safe(Discourse.Utilities.avatarImg({
|
||||||
size: size,
|
size: size,
|
||||||
avatarTemplate: avatarTemplate
|
avatarTemplate: avatarTemplate
|
||||||
}));
|
}));
|
||||||
|
@ -208,7 +212,7 @@ Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
|
||||||
* Used when we only have a template
|
* Used when we only have a template
|
||||||
*/
|
*/
|
||||||
Em.Handlebars.helper('bound-avatar-template', function(avatarTemplate, size) {
|
Em.Handlebars.helper('bound-avatar-template', function(avatarTemplate, size) {
|
||||||
return new Handlebars.SafeString(Discourse.Utilities.avatarImg({
|
return new safe(Discourse.Utilities.avatarImg({
|
||||||
size: size,
|
size: size,
|
||||||
avatarTemplate: avatarTemplate
|
avatarTemplate: avatarTemplate
|
||||||
}));
|
}));
|
||||||
|
@ -243,7 +247,7 @@ Em.Handlebars.helper('bound-raw-date', function (date) {
|
||||||
**/
|
**/
|
||||||
Handlebars.registerHelper('age', function(property, options) {
|
Handlebars.registerHelper('age', function(property, options) {
|
||||||
var dt = new Date(Ember.Handlebars.get(this, property, options));
|
var dt = new Date(Ember.Handlebars.get(this, property, options));
|
||||||
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(dt));
|
return new safe(Discourse.Formatter.autoUpdatingRelativeAge(dt));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -254,7 +258,7 @@ Handlebars.registerHelper('age', function(property, options) {
|
||||||
**/
|
**/
|
||||||
Handlebars.registerHelper('age-with-tooltip', function(property, options) {
|
Handlebars.registerHelper('age-with-tooltip', function(property, options) {
|
||||||
var dt = new Date(Ember.Handlebars.get(this, property, options));
|
var dt = new Date(Ember.Handlebars.get(this, property, options));
|
||||||
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(dt, {title: true}));
|
return new safe(Discourse.Formatter.autoUpdatingRelativeAge(dt, {title: true}));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -286,7 +290,7 @@ Handlebars.registerHelper('number', function(property, options) {
|
||||||
}
|
}
|
||||||
result += ">" + n + "</span>";
|
result += ">" + n + "</span>";
|
||||||
|
|
||||||
return new Handlebars.SafeString(result);
|
return new safe(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -310,12 +314,12 @@ Handlebars.registerHelper('date', function(property, options) {
|
||||||
var val = Ember.Handlebars.get(this, property, options);
|
var val = Ember.Handlebars.get(this, property, options);
|
||||||
if (val) {
|
if (val) {
|
||||||
var date = new Date(val);
|
var date = new Date(val);
|
||||||
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(date, {format: 'medium', title: true, leaveAgo: leaveAgo}));
|
return new safe(Discourse.Formatter.autoUpdatingRelativeAge(date, {format: 'medium', title: true, leaveAgo: leaveAgo}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Em.Handlebars.helper('bound-date', function(dt) {
|
Em.Handlebars.helper('bound-date', function(dt) {
|
||||||
return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(new Date(dt), {format: 'medium', title: true }));
|
return new safe(Discourse.Formatter.autoUpdatingRelativeAge(new Date(dt), {format: 'medium', title: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -337,7 +341,7 @@ Handlebars.registerHelper('custom-html', function(name, contextString, options)
|
||||||
});
|
});
|
||||||
|
|
||||||
Em.Handlebars.helper('human-size', function(size) {
|
Em.Handlebars.helper('human-size', function(size) {
|
||||||
return new Handlebars.SafeString(I18n.toHumanSize(size));
|
return new safe(I18n.toHumanSize(size));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -356,7 +360,7 @@ Handlebars.registerHelper('link-domain', function(property, options) {
|
||||||
if (!Em.isEmpty(domain)) {
|
if (!Em.isEmpty(domain)) {
|
||||||
var s = domain.split('.');
|
var s = domain.split('.');
|
||||||
domain = s[s.length-2] + "." + s[s.length-1];
|
domain = s[s.length-2] + "." + s[s.length-1];
|
||||||
return new Handlebars.SafeString("<span class='domain'>" + domain + "</span>");
|
return new safe("<span class='domain'>" + domain + "</span>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -378,5 +382,5 @@ Handlebars.registerHelper('icon', function(icon, options) {
|
||||||
if (labelKey) {
|
if (labelKey) {
|
||||||
html += "<span class='sr-only'>" + I18n.t(labelKey) + "</span>";
|
html += "<span class='sr-only'>" + I18n.t(labelKey) + "</span>";
|
||||||
}
|
}
|
||||||
return new Handlebars.SafeString(html);
|
return new safe(html);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
export default {
|
export default {
|
||||||
name: "inject-app-events",
|
name: "inject-app-events",
|
||||||
initialize: function(container, application) {
|
initialize: function(container, application) {
|
||||||
var AppEvents = Ember.Object.extend(Ember.Evented);
|
var appEvents = Ember.Object.createWithMixins(Ember.Evented);
|
||||||
application.register('app-events:main', AppEvents, { singleton: true });
|
application.register('app-events:main', appEvents, { instantiate: false });
|
||||||
|
|
||||||
application.inject('controller', 'appEvents', 'app-events:main');
|
application.inject('controller', 'appEvents', 'app-events:main');
|
||||||
application.inject('component', 'appEvents', 'app-events:main');
|
application.inject('component', 'appEvents', 'app-events:main');
|
||||||
application.inject('route', 'appEvents', 'app-events:main');
|
application.inject('route', 'appEvents', 'app-events:main');
|
||||||
application.inject('view', 'appEvents', 'app-events:main');
|
application.inject('view', 'appEvents', 'app-events:main');
|
||||||
application.inject('model', 'appEvents', 'app-events:main');
|
application.inject('model', 'appEvents', 'app-events:main');
|
||||||
|
|
||||||
|
Discourse.URL.appEvents = appEvents;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,15 +14,6 @@ var _validClasses = {},
|
||||||
function validateAttribute(tagName, attribName, value) {
|
function validateAttribute(tagName, attribName, value) {
|
||||||
var tag = _validTags[tagName];
|
var tag = _validTags[tagName];
|
||||||
|
|
||||||
// Handle possible attacks
|
|
||||||
// if you include html in your markdown, it better be valid
|
|
||||||
//
|
|
||||||
// We are SUPER strict cause nokogiri will sometimes "correct"
|
|
||||||
// this stuff "incorrectly"
|
|
||||||
if(/[<>"'`]/.test(value)){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle classes
|
// Handle classes
|
||||||
if (attribName === "class") {
|
if (attribName === "class") {
|
||||||
if (_validClasses[value]) { return value; }
|
if (_validClasses[value]) { return value; }
|
||||||
|
|
|
@ -71,6 +71,20 @@ Discourse.URL = Em.Object.createWithMixins({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Scroll to the same page, different anchor
|
||||||
|
scrollToId: function(id) {
|
||||||
|
if (Em.isEmpty(id)) { return; }
|
||||||
|
|
||||||
|
jumpScheduled = true;
|
||||||
|
Em.run.schedule('afterRender', function() {
|
||||||
|
var $elem = $(id);
|
||||||
|
if ($elem.length > 0) {
|
||||||
|
$('html,body').scrollTop($elem.offset().top - $('header').height() - 15);
|
||||||
|
jumpScheduled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Our custom routeTo method is used to intelligently overwrite default routing
|
Our custom routeTo method is used to intelligently overwrite default routing
|
||||||
behavior.
|
behavior.
|
||||||
|
@ -98,12 +112,7 @@ Discourse.URL = Em.Object.createWithMixins({
|
||||||
|
|
||||||
// Scroll to the same page, different anchor
|
// Scroll to the same page, different anchor
|
||||||
if (path.indexOf('#') === 0) {
|
if (path.indexOf('#') === 0) {
|
||||||
var $elem = $(path);
|
this.scrollToId(path);
|
||||||
if ($elem.length > 0) {
|
|
||||||
Em.run.schedule('afterRender', function() {
|
|
||||||
$('html,body').scrollTop($elem.offset().top - $('header').height() - 15);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,8 +145,10 @@ Discourse.URL = Em.Object.createWithMixins({
|
||||||
// TODO: Extract into rules we can inject into the URL handler
|
// TODO: Extract into rules we can inject into the URL handler
|
||||||
if (this.navigatedToHome(oldPath, path)) { return; }
|
if (this.navigatedToHome(oldPath, path)) { return; }
|
||||||
|
|
||||||
if (path.match(/^\/?users\/[^\/]+$/)) {
|
if (oldPath === path) {
|
||||||
path += "/activity";
|
// If navigating to the same path send an app event. Views can watch it
|
||||||
|
// and tell their controllers to refresh
|
||||||
|
this.appEvents.trigger('url:refresh');
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.handleURL(path);
|
return this.handleURL(path);
|
||||||
|
@ -235,12 +246,7 @@ Discourse.URL = Em.Object.createWithMixins({
|
||||||
var homepage = Discourse.Utilities.defaultHomepage();
|
var homepage = Discourse.Utilities.defaultHomepage();
|
||||||
|
|
||||||
if (window.history && window.history.pushState && path === "/" && (oldPath === "/" || oldPath === "/" + homepage)) {
|
if (window.history && window.history.pushState && path === "/" && (oldPath === "/" || oldPath === "/" + homepage)) {
|
||||||
// refresh the list
|
this.appEvents.trigger('url:refresh');
|
||||||
switch (homepage) {
|
|
||||||
case "top" : { this.controllerFor('discovery/top').send('refresh'); break; }
|
|
||||||
case "categories": { this.controllerFor('discovery/categories').send('refresh'); break; }
|
|
||||||
default: { this.controllerFor('discovery/topics').send('refresh'); break; }
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// A Mixin that a view can use to listen for 'url:refresh' when
|
||||||
|
// it is on screen, and will send an action to the controller to
|
||||||
|
// refresh its data.
|
||||||
|
//
|
||||||
|
// This is useful if you want to get around Ember's default
|
||||||
|
// behavior of not refreshing when navigating to the same place.
|
||||||
|
export default Em.Mixin.create({
|
||||||
|
_initURLRefresh: function() {
|
||||||
|
this.appEvents.on('url:refresh', this, '_urlRefresh');
|
||||||
|
}.on('didInsertElement'),
|
||||||
|
|
||||||
|
_tearDownURLRefresh: function() {
|
||||||
|
this.appEvents.off('url:refresh', this, '_urlRefresh');
|
||||||
|
}.on('willDestroyElement'),
|
||||||
|
|
||||||
|
_urlRefresh: function() {
|
||||||
|
this.get('controller').send('refresh');
|
||||||
|
}
|
||||||
|
});
|
|
@ -32,7 +32,15 @@ Ember.DiscourseLocation = Ember.Object.extend({
|
||||||
*/
|
*/
|
||||||
initState: function() {
|
initState: function() {
|
||||||
set(this, 'history', get(this, 'history') || window.history);
|
set(this, 'history', get(this, 'history') || window.history);
|
||||||
this.replaceState(this.formatURL(this.getURL()));
|
|
||||||
|
var url = this.formatURL(this.getURL()),
|
||||||
|
loc = get(this, 'location');
|
||||||
|
|
||||||
|
if (loc && loc.hash) {
|
||||||
|
url += loc.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.replaceState(url);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -21,6 +21,13 @@ Discourse.StaticController.PAGES.forEach(function(page) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
activate: function() {
|
||||||
|
this._super();
|
||||||
|
|
||||||
|
// Scroll to an element if exists
|
||||||
|
Discourse.URL.scrollToId(document.location.hash);
|
||||||
|
},
|
||||||
|
|
||||||
model: function() {
|
model: function() {
|
||||||
return Discourse.StaticPage.find(page);
|
return Discourse.StaticPage.find(page);
|
||||||
},
|
},
|
||||||
|
|
|
@ -98,7 +98,11 @@ Discourse.TopicRoute = Discourse.Route.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
willTransition: function() { isTransitioning = true; return true; }
|
willTransition: function() {
|
||||||
|
Em.run.cancel(scheduledReplace);
|
||||||
|
isTransitioning = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// replaceState can be very slow on Android Chrome. This function debounces replaceState
|
// replaceState can be very slow on Android Chrome. This function debounces replaceState
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export default Discourse.View.extend({
|
import UrlRefresh from 'discourse/mixins/url-refresh';
|
||||||
|
|
||||||
|
export default Discourse.View.extend(UrlRefresh, {
|
||||||
|
|
||||||
orderingChanged: function(){
|
orderingChanged: function(){
|
||||||
if (this.get("controller.ordering")) {
|
if (this.get("controller.ordering")) {
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export default Discourse.View.extend(Discourse.ScrollTop);
|
import UrlRefresh from 'discourse/mixins/url-refresh';
|
||||||
|
|
||||||
|
export default Discourse.View.extend(Discourse.ScrollTop, UrlRefresh);
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
/**
|
import UrlRefresh from 'discourse/mixins/url-refresh';
|
||||||
This view handles rendering of a list of topics under discovery, with support
|
|
||||||
for loading more as well as remembering your scroll position.
|
|
||||||
|
|
||||||
@class DiscoveryTopicsView
|
export default Discourse.View.extend(Discourse.LoadMore, UrlRefresh, {
|
||||||
@extends Discourse.View
|
|
||||||
@namespace Discourse
|
|
||||||
@module Discourse
|
|
||||||
**/
|
|
||||||
export default Discourse.View.extend(Discourse.LoadMore, {
|
|
||||||
eyelineSelector: '.topic-list-item',
|
eyelineSelector: '.topic-list-item',
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
|
|
@ -5,27 +5,8 @@ export default Discourse.View.extend({
|
||||||
classNameBindings: ['controller.visible::hidden', 'controller.showBadges'],
|
classNameBindings: ['controller.visible::hidden', 'controller.showBadges'],
|
||||||
|
|
||||||
_setup: function() {
|
_setup: function() {
|
||||||
var self = this,
|
var self = this;
|
||||||
width = this.$().width();
|
this.appEvents.on('poster:expand', this, '_posterExpand');
|
||||||
|
|
||||||
this.appEvents.on('poster:expand', function(target) {
|
|
||||||
if (!target) { return; }
|
|
||||||
Em.run.schedule('afterRender', function() {
|
|
||||||
if (target) {
|
|
||||||
var position = target.offset();
|
|
||||||
if (position) {
|
|
||||||
position.left += target.width() + 10;
|
|
||||||
|
|
||||||
var overage = ($(window).width() - 50) - (position.left + width);
|
|
||||||
if (overage < 0) {
|
|
||||||
position.left += overage;
|
|
||||||
position.top += target.height() + 5;
|
|
||||||
}
|
|
||||||
self.$().css(position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('html').off(clickOutsideEventName).on(clickOutsideEventName, function(e) {
|
$('html').off(clickOutsideEventName).on(clickOutsideEventName, function(e) {
|
||||||
if (self.get('controller.visible')) {
|
if (self.get('controller.visible')) {
|
||||||
|
@ -40,9 +21,30 @@ export default Discourse.View.extend({
|
||||||
});
|
});
|
||||||
}.on('didInsertElement'),
|
}.on('didInsertElement'),
|
||||||
|
|
||||||
|
_posterExpand: function(target) {
|
||||||
|
if (!target) { return; }
|
||||||
|
var self = this,
|
||||||
|
width = this.$().width();
|
||||||
|
Em.run.schedule('afterRender', function() {
|
||||||
|
if (target) {
|
||||||
|
var position = target.offset();
|
||||||
|
if (position) {
|
||||||
|
position.left += target.width() + 10;
|
||||||
|
|
||||||
|
var overage = ($(window).width() - 50) - (position.left + width);
|
||||||
|
if (overage < 0) {
|
||||||
|
position.left += overage;
|
||||||
|
position.top += target.height() + 5;
|
||||||
|
}
|
||||||
|
self.$().css(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_removeEvents: function() {
|
_removeEvents: function() {
|
||||||
$('html').off(clickOutsideEventName);
|
$('html').off(clickOutsideEventName);
|
||||||
this.appEvents.off('poster:expand');
|
this.appEvents.off('poster:expand', this, '_posterExpand');
|
||||||
}.on('willDestroyElement')
|
}.on('willDestroyElement')
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,6 +11,13 @@
|
||||||
padding: 12px 12px 5px 12px;
|
padding: 12px 12px 5px 12px;
|
||||||
border: 1px solid scale-color-diff();
|
border: 1px solid scale-color-diff();
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
float: left;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
module Jobs
|
||||||
|
|
||||||
|
class CalculateAvgTime < Jobs::Scheduled
|
||||||
|
every 1.day
|
||||||
|
|
||||||
|
# PERF: these calculations can become exceedingly expnsive
|
||||||
|
# they run a huge gemoetric mean and are hard to optimise
|
||||||
|
# defer to only run once a day
|
||||||
|
def execute(args)
|
||||||
|
# Update the average times
|
||||||
|
Post.calculate_avg_time(2.days.ago)
|
||||||
|
Topic.calculate_avg_time(2.days.ago)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -8,9 +8,6 @@ module Jobs
|
||||||
every 15.minutes
|
every 15.minutes
|
||||||
|
|
||||||
def execute(args)
|
def execute(args)
|
||||||
# Update the average times
|
|
||||||
Post.calculate_avg_time(1.day.ago)
|
|
||||||
Topic.calculate_avg_time(1.day.ago)
|
|
||||||
|
|
||||||
# Feature topics in categories
|
# Feature topics in categories
|
||||||
CategoryFeaturedTopic.feature_topics
|
CategoryFeaturedTopic.feature_topics
|
||||||
|
|
|
@ -23,8 +23,15 @@ class ExcerptParser < Nokogiri::XML::SAX::Document
|
||||||
me.excerpt
|
me.excerpt
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def escape_attribute(v)
|
||||||
|
v.gsub("&", "&")
|
||||||
|
.gsub("\"", """)
|
||||||
|
.gsub("<", "<")
|
||||||
|
.gsub(">", ">")
|
||||||
|
end
|
||||||
|
|
||||||
def include_tag(name, attributes)
|
def include_tag(name, attributes)
|
||||||
characters("<#{name} #{attributes.map{|k,v| "#{k}='#{v}'"}.join(' ')}>", false, false, false)
|
characters("<#{name} #{attributes.map{|k,v| "#{k}=\"#{escape_attribute(v)}\""}.join(' ')}>", false, false, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def start_element(name, attributes=[])
|
def start_element(name, attributes=[])
|
||||||
|
|
|
@ -264,8 +264,21 @@ module SiteSettingExtension
|
||||||
refresh_settings.include?(name.to_sym)
|
refresh_settings.include?(name.to_sym)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_value(name, value)
|
||||||
|
# filter domain name
|
||||||
|
if %w[disabled_image_download_domains onebox_domains_whitelist exclude_rel_nofollow_domains email_domains_blacklist email_domains_whitelist white_listed_spam_host_domains].include? name
|
||||||
|
domain_array = []
|
||||||
|
value.split('|').each { |url|
|
||||||
|
domain_array.push(get_hostname(url))
|
||||||
|
}
|
||||||
|
value = domain_array.join("|")
|
||||||
|
end
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
def set(name, value)
|
def set(name, value)
|
||||||
if has_setting?(name)
|
if has_setting?(name)
|
||||||
|
value = filter_value(name, value)
|
||||||
self.send("#{name}=", value)
|
self.send("#{name}=", value)
|
||||||
Discourse.request_refresh! if requires_refresh?(name)
|
Discourse.request_refresh! if requires_refresh?(name)
|
||||||
else
|
else
|
||||||
|
@ -365,5 +378,13 @@ module SiteSettingExtension
|
||||||
enums[name]
|
enums[name]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_hostname(url)
|
||||||
|
unless (URI.parse(url).scheme rescue nil).nil?
|
||||||
|
url = "http://#{url}" if URI.parse(url).scheme.nil?
|
||||||
|
url = URI.parse(url).host
|
||||||
|
end
|
||||||
|
return url
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,15 @@ describe PrettyText do
|
||||||
|
|
||||||
describe "Excerpt" do
|
describe "Excerpt" do
|
||||||
|
|
||||||
|
it "sanitizes attempts to inject invalid attributes" do
|
||||||
|
|
||||||
|
spinner = "<a href=\"http://thedailywtf.com/\" data-bbcode=\"' class='fa fa-spin\">WTF</a>"
|
||||||
|
PrettyText.excerpt(spinner, 20).should match_html spinner
|
||||||
|
|
||||||
|
spinner = %q{<a href="http://thedailywtf.com/" title="' class="fa fa-spin"><img src='http://thedailywtf.com/Resources/Images/Primary/logo.gif"></a>}
|
||||||
|
PrettyText.excerpt(spinner, 20).should match_html spinner
|
||||||
|
end
|
||||||
|
|
||||||
context "images" do
|
context "images" do
|
||||||
|
|
||||||
it "should dump images" do
|
it "should dump images" do
|
||||||
|
@ -94,8 +103,8 @@ describe PrettyText do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should keep spoilers" do
|
it "should keep spoilers" do
|
||||||
PrettyText.excerpt("<div class='spoiler'><img src='http://cnn.com/a.gif'></div>", 100).should == "<span class='spoiler'>[image]</span>"
|
PrettyText.excerpt("<div class='spoiler'><img src='http://cnn.com/a.gif'></div>", 100).should match_html "<span class='spoiler'>[image]</span>"
|
||||||
PrettyText.excerpt("<span class='spoiler'>spoiler</div>", 100).should == "<span class='spoiler'>spoiler</span>"
|
PrettyText.excerpt("<span class='spoiler'>spoiler</div>", 100).should match_html "<span class='spoiler'>spoiler</span>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -104,7 +113,7 @@ describe PrettyText do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should preserve links" do
|
it "should preserve links" do
|
||||||
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",100).should == "<a href='http://cnn.com'>cnn</a>"
|
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",100).should match_html "<a href='http://cnn.com'>cnn</a>"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should deal with special keys properly" do
|
it "should deal with special keys properly" do
|
||||||
|
@ -125,15 +134,15 @@ describe PrettyText do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should not count the surrounds of a link" do
|
it "should not count the surrounds of a link" do
|
||||||
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",3).should == "<a href='http://cnn.com'>cnn</a>"
|
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",3).should match_html "<a href='http://cnn.com'>cnn</a>"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "uses an ellipsis instead of html entities if provided with the option" do
|
it "uses an ellipsis instead of html entities if provided with the option" do
|
||||||
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>", 2, text_entities: true).should == "<a href='http://cnn.com'>cn...</a>"
|
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>", 2, text_entities: true).should match_html "<a href='http://cnn.com'>cn...</a>"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should truncate links" do
|
it "should truncate links" do
|
||||||
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",2).should == "<a href='http://cnn.com'>cn…</a>"
|
PrettyText.excerpt("<a href='http://cnn.com'>cnn</a>",2).should match_html "<a href='http://cnn.com'>cn…</a>"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn't extract empty quotes as links" do
|
it "doesn't extract empty quotes as links" do
|
||||||
|
@ -294,9 +303,6 @@ describe PrettyText do
|
||||||
PrettyText.cook("**你hello**").should match_html "<p><strong>你hello</strong></p>"
|
PrettyText.cook("**你hello**").should match_html "<p><strong>你hello</strong></p>"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "sanitizes attempts to inject invalid attributes" do
|
|
||||||
PrettyText.cook("<a href=\"http://thedailywtf.com/\" data-bbcode=\"' class='fa fa-spin\">WTF</a>").should == "<p><a href=\"http://thedailywtf.com/\" rel=\"nofollow\">WTF</a></p>"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -322,4 +322,21 @@ describe SiteSettingExtension do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "filter domain name" do
|
||||||
|
before do
|
||||||
|
settings.setting(:white_listed_spam_host_domains, "www.example.com")
|
||||||
|
settings.refresh!
|
||||||
|
end
|
||||||
|
|
||||||
|
it "filters domain" do
|
||||||
|
settings.set("white_listed_spam_host_domains", "http://www.discourse.org/")
|
||||||
|
settings.white_listed_spam_host_domains.should == "www.discourse.org"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns invalid domain as is, without throwing exception" do
|
||||||
|
settings.set("white_listed_spam_host_domains", "test!url")
|
||||||
|
settings.white_listed_spam_host_domains.should == "test!url"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -57,7 +57,7 @@ describe UserProfile do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a user that has a link in their bio' do
|
context 'with a user that has a link in their bio' do
|
||||||
let(:user_profile) { Fabricate.build(:user_profile, bio_raw: "im sissy and i love http://ponycorns.com") }
|
let(:user_profile) { Fabricate.build(:user_profile, bio_raw: "I love http://discourse.org") }
|
||||||
let(:user) do
|
let(:user) do
|
||||||
user = Fabricate.build(:user, user_profile: user_profile)
|
user = Fabricate.build(:user, user_profile: user_profile)
|
||||||
user_profile.user = user
|
user_profile.user = user
|
||||||
|
@ -66,22 +66,22 @@ describe UserProfile do
|
||||||
|
|
||||||
let(:created_user) do
|
let(:created_user) do
|
||||||
user = Fabricate(:user)
|
user = Fabricate(:user)
|
||||||
user.user_profile.bio_raw = 'im sissy and i love http://ponycorns.com'
|
user.user_profile.bio_raw = 'I love http://discourse.org'
|
||||||
user.user_profile.save!
|
user.user_profile.save!
|
||||||
user
|
user
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'includes the link as nofollow if the user is not new' do
|
it 'includes the link as nofollow if the user is not new' do
|
||||||
user.user_profile.send(:cook)
|
user.user_profile.send(:cook)
|
||||||
expect(user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com' rel='nofollow'>http://ponycorns.com</a>")
|
expect(user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org' rel='nofollow'>http://discourse.org</a>")
|
||||||
expect(user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\" rel=\"nofollow\">http://ponycorns.com</a></p>")
|
expect(user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\" rel=\"nofollow\">http://discourse.org</a></p>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes the link if the user is new' do
|
it 'removes the link if the user is new' do
|
||||||
user.trust_level = TrustLevel.levels[:newuser]
|
user.trust_level = TrustLevel.levels[:newuser]
|
||||||
user_profile.send(:cook)
|
user_profile.send(:cook)
|
||||||
expect(user_profile.bio_excerpt).to eq("im sissy and i love http://ponycorns.com")
|
expect(user_profile.bio_excerpt).to match_html("I love http://discourse.org")
|
||||||
expect(user_profile.bio_processed).to eq("<p>im sissy and i love http://ponycorns.com</p>")
|
expect(user_profile.bio_processed).to eq("<p>I love http://discourse.org</p>")
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'leader_links_no_follow is false' do
|
context 'leader_links_no_follow is false' do
|
||||||
|
@ -90,22 +90,22 @@ describe UserProfile do
|
||||||
it 'includes the link without nofollow if the user is trust level 3 or higher' do
|
it 'includes the link without nofollow if the user is trust level 3 or higher' do
|
||||||
user.trust_level = TrustLevel.levels[:leader]
|
user.trust_level = TrustLevel.levels[:leader]
|
||||||
user_profile.send(:cook)
|
user_profile.send(:cook)
|
||||||
expect(user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com'>http://ponycorns.com</a>")
|
expect(user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org'>http://discourse.org</a>")
|
||||||
expect(user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\">http://ponycorns.com</a></p>")
|
expect(user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\">http://discourse.org</a></p>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'removes nofollow from links in bio when trust level is increased' do
|
it 'removes nofollow from links in bio when trust level is increased' do
|
||||||
created_user.change_trust_level!(:leader)
|
created_user.change_trust_level!(:leader)
|
||||||
expect(created_user.user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com'>http://ponycorns.com</a>")
|
expect(created_user.user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org'>http://discourse.org</a>")
|
||||||
expect(created_user.user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\">http://ponycorns.com</a></p>")
|
expect(created_user.user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\">http://discourse.org</a></p>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'adds nofollow to links in bio when trust level is decreased' do
|
it 'adds nofollow to links in bio when trust level is decreased' do
|
||||||
created_user.trust_level = TrustLevel.levels[:leader]
|
created_user.trust_level = TrustLevel.levels[:leader]
|
||||||
created_user.save
|
created_user.save
|
||||||
created_user.change_trust_level!(:regular)
|
created_user.change_trust_level!(:regular)
|
||||||
expect(created_user.user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com' rel='nofollow'>http://ponycorns.com</a>")
|
expect(created_user.user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org' rel='nofollow'>http://discourse.org</a>")
|
||||||
expect(created_user.user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\" rel=\"nofollow\">http://ponycorns.com</a></p>")
|
expect(created_user.user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\" rel=\"nofollow\">http://discourse.org</a></p>")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -115,8 +115,8 @@ describe UserProfile do
|
||||||
it 'includes the link with nofollow if the user is trust level 3 or higher' do
|
it 'includes the link with nofollow if the user is trust level 3 or higher' do
|
||||||
user.trust_level = TrustLevel.levels[:leader]
|
user.trust_level = TrustLevel.levels[:leader]
|
||||||
user_profile.send(:cook)
|
user_profile.send(:cook)
|
||||||
expect(user_profile.bio_excerpt).to eq("im sissy and i love <a href='http://ponycorns.com' rel='nofollow'>http://ponycorns.com</a>")
|
expect(user_profile.bio_excerpt).to match_html("I love <a href='http://discourse.org' rel='nofollow'>http://discourse.org</a>")
|
||||||
expect(user_profile.bio_processed).to eq("<p>im sissy and i love <a href=\"http://ponycorns.com\" rel=\"nofollow\">http://ponycorns.com</a></p>")
|
expect(user_profile.bio_processed).to match_html("<p>I love <a href=\"http://discourse.org\" rel=\"nofollow\">http://discourse.org</a></p>")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,19 +28,21 @@ test("isInternal with a HTTPS url", function() {
|
||||||
// ok(Discourse.URL.routeTo("/t/topic-title/42"), "can route relative");
|
// ok(Discourse.URL.routeTo("/t/topic-title/42"), "can route relative");
|
||||||
// });
|
// });
|
||||||
|
|
||||||
test("navigatedToHome", function() {
|
// TODO pending: this works but the test is too mocky and needs to be fixed
|
||||||
var fakeDiscoveryController = { send: function() { return true; } };
|
|
||||||
var mock = sinon.mock(fakeDiscoveryController);
|
|
||||||
this.stub(Discourse.URL, "controllerFor").returns(fakeDiscoveryController);
|
|
||||||
|
|
||||||
mock.expects("send").withArgs('refresh').twice();
|
// test("navigatedToHome", function() {
|
||||||
ok(Discourse.URL.navigatedToHome("/", "/"));
|
// var fakeDiscoveryController = { send: function() { return true; } };
|
||||||
|
// var mock = sinon.mock(fakeDiscoveryController);
|
||||||
var homepage = "/" + Discourse.Utilities.defaultHomepage();
|
// this.stub(Discourse.URL, "controllerFor").returns(fakeDiscoveryController);
|
||||||
ok(Discourse.URL.navigatedToHome(homepage, "/"));
|
//
|
||||||
|
// mock.expects("send").withArgs('refresh').twice();
|
||||||
not(Discourse.URL.navigatedToHome("/old", "/new"));
|
// ok(Discourse.URL.navigatedToHome("/", "/"));
|
||||||
|
//
|
||||||
// make sure we called the .refresh() method
|
// var homepage = "/" + Discourse.Utilities.defaultHomepage();
|
||||||
mock.verify();
|
// ok(Discourse.URL.navigatedToHome(homepage, "/"));
|
||||||
});
|
//
|
||||||
|
// not(Discourse.URL.navigatedToHome("/old", "/new"));
|
||||||
|
//
|
||||||
|
// // make sure we called the .refresh() method
|
||||||
|
// mock.verify();
|
||||||
|
// });
|
||||||
|
|
Loading…
Reference in New Issue