Merge branch 'master' of github.com:discourse/discourse

This commit is contained in:
Neil Lalonde 2015-01-29 17:39:52 -05:00
commit 67b262b93e
14 changed files with 110 additions and 43 deletions

View file

@ -24,6 +24,12 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
return u + url; return u + url;
}, },
getURLWithCDN: function(url) {
url = this.getURL(url);
if (Discourse.CDN) { url = Discourse.CDN + url; }
return url;
},
Resolver: DiscourseResolver, Resolver: DiscourseResolver,
_titleChanged: function() { _titleChanged: function() {

View file

@ -1,4 +1,5 @@
var esc = Handlebars.Utils.escapeExpression; var esc = Handlebars.Utils.escapeExpression;
Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(contents, bbParams, options) { Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(contents, bbParams, options) {
var params = {'class': 'quote'}, var params = {'class': 'quote'},
username = null; username = null;

View file

@ -72,10 +72,9 @@ Discourse.User = Discourse.Model.extend({
@type {String} @type {String}
**/ **/
profileBackground: function() { profileBackground: function() {
var background = this.get('profile_background'); var url = this.get('profile_background');
if(Em.isEmpty(background) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; } if (Em.isEmpty(url) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; }
return 'background-image: url(' + Discourse.getURLWithCDN(url) + ')';
return 'background-image: url(' + background + ')';
}.property('profile_background'), }.property('profile_background'),
/** /**
@ -442,6 +441,7 @@ Discourse.User.reopenClass(Discourse.Singleton, {
avatarTemplate: function(username, uploadedAvatarId) { avatarTemplate: function(username, uploadedAvatarId) {
var url; var url;
if (uploadedAvatarId) { if (uploadedAvatarId) {
url = "/user_avatar/" + url = "/user_avatar/" +
Discourse.BaseUrl + Discourse.BaseUrl +
@ -456,11 +456,7 @@ Discourse.User.reopenClass(Discourse.Singleton, {
Discourse.LetterAvatarVersion + ".png"; Discourse.LetterAvatarVersion + ".png";
} }
url = Discourse.getURL(url); return Discourse.getURLWithCDN(url);
if (Discourse.CDN) {
url = Discourse.CDN + url;
}
return url;
}, },
/** /**

View file

@ -11,6 +11,7 @@ export default Discourse.View.extend(CleansUp, {
addBackground: function() { addBackground: function() {
var url = this.get('controller.user.card_background'); var url = this.get('controller.user.card_background');
if (!this.get('allowBackgrounds')) { return; } if (!this.get('allowBackgrounds')) { return; }
var $this = this.$(); var $this = this.$();
@ -19,7 +20,7 @@ export default Discourse.View.extend(CleansUp, {
if (Ember.isEmpty(url)) { if (Ember.isEmpty(url)) {
$this.css('background-image', '').addClass('no-bg'); $this.css('background-image', '').addClass('no-bg');
} else { } else {
$this.css('background-image', "url(" + url + ")").removeClass('no-bg'); $this.css('background-image', "url(" + Discourse.getURLWithCDN(url) + ")").removeClass('no-bg');
} }
}.observes('controller.user.card_background'), }.observes('controller.user.card_background'),

View file

@ -155,7 +155,7 @@ class ApplicationController < ActionController::Base
# If we are rendering HTML, preload the session data # If we are rendering HTML, preload the session data
def preload_json def preload_json
# We don't preload JSON on xhr or JSON request # We don't preload JSON on xhr or JSON request
return if request.xhr? return if request.xhr? || request.format.json?
preload_anonymous_data preload_anonymous_data

View file

@ -5,7 +5,7 @@ require_dependency 'distributed_memoizer'
class PostsController < ApplicationController class PostsController < ApplicationController
# Need to be logged in for all actions here # Need to be logged in for all actions here
before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :reply_history, :revisions, :latest_revision, :expand_embed, :markdown, :raw, :cooked] before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :reply_history, :revisions, :latest_revision, :expand_embed, :markdown_id, :markdown_num, :cooked, :latest]
skip_before_filter :check_xhr, only: [:markdown_id, :markdown_num, :short_link] skip_before_filter :check_xhr, only: [:markdown_id, :markdown_num, :short_link]
@ -25,6 +25,33 @@ class PostsController < ApplicationController
end end
end end
def latest
params.permit(:before)
last_post_id = params[:before].to_i
last_post_id = Post.last.id if last_post_id <= 0
# last 50 post IDs only, to avoid counting deleted posts in security check
posts = Post.order(created_at: :desc)
.where('posts.id <= ?', last_post_id)
.where('posts.id > ?', last_post_id - 50)
.includes(topic: :category)
.includes(:user)
.limit(50)
# Remove posts the user doesn't have permission to see
# This isn't leaking any information we weren't already through the post ID numbers
posts = posts.reject { |post| !guardian.can_see?(post) }
counts = PostAction.counts_for(posts, current_user)
render_json_dump(serialize_data(posts,
PostSerializer,
scope: guardian,
root: 'latest_posts',
add_raw: true,
all_post_actions: counts)
)
end
def cooked def cooked
post = find_post_from_params post = find_post_from_params
render json: {cooked: post.cooked} render json: {cooked: post.cooked}

View file

@ -78,17 +78,6 @@ module HasCustomFields
!@custom_fields || @custom_fields_orig == @custom_fields !@custom_fields || @custom_fields_orig == @custom_fields
end end
protected
def refresh_custom_fields_from_db
target = Hash.new
_custom_fields.pluck(:name,:value).each do |key, value|
self.class.append_custom_field(target, key, value)
end
@custom_fields_orig = target
@custom_fields = @custom_fields_orig.dup
end
def save_custom_fields def save_custom_fields
if !custom_fields_clean? if !custom_fields_clean?
dup = @custom_fields.dup dup = @custom_fields.dup
@ -134,4 +123,16 @@ module HasCustomFields
refresh_custom_fields_from_db refresh_custom_fields_from_db
end end
end end
protected
def refresh_custom_fields_from_db
target = Hash.new
_custom_fields.pluck(:name,:value).each do |key, value|
self.class.append_custom_field(target, key, value)
end
@custom_fields_orig = target
@custom_fields = @custom_fields_orig.dup
end
end end

View file

@ -62,7 +62,7 @@ class User < ActiveRecord::Base
delegate :last_sent_email_address, :to => :email_logs delegate :last_sent_email_address, :to => :email_logs
before_validation :downcase_email before_validation :strip_downcase_email
validates_presence_of :username validates_presence_of :username
validate :username_validator validate :username_validator
@ -764,8 +764,11 @@ class User < ActiveRecord::Base
self.username_lower = username.downcase self.username_lower = username.downcase
end end
def downcase_email def strip_downcase_email
self.email = self.email.downcase if self.email if self.email
self.email = self.email.strip
self.email = self.email.downcase
end
end end
def username_validator def username_validator

View file

@ -1,12 +1,17 @@
class PostSerializer < BasicPostSerializer class PostSerializer < BasicPostSerializer
# To pass in additional information we might need # To pass in additional information we might need
attr_accessor :topic_view, INSTANCE_VARS = [:topic_view,
:parent_post, :parent_post,
:add_raw, :add_raw,
:single_post_link_counts, :single_post_link_counts,
:draft_sequence, :draft_sequence,
:post_actions :post_actions,
:all_post_actions]
INSTANCE_VARS.each do |v|
self.send(:attr_accessor, v)
end
attributes :post_number, attributes :post_number,
:post_type, :post_type,
@ -54,6 +59,15 @@ class PostSerializer < BasicPostSerializer
:static_doc, :static_doc,
:via_email :via_email
def initialize(object, opts)
super(object, opts)
PostSerializer::INSTANCE_VARS.each do |name|
if opts.include? name
self.send("#{name}=", opts[name])
end
end
end
def topic_slug def topic_slug
object.try(:topic).try(:slug) object.try(:topic).try(:slug)
end end
@ -155,6 +169,13 @@ class PostSerializer < BasicPostSerializer
scope.is_staff? && object.deleted_by.present? scope.is_staff? && object.deleted_by.present?
end end
# Helper function to decide between #post_actions and @all_post_actions
def actions
return post_actions if post_actions.present?
return all_post_actions[object.id] if all_post_actions.present?
nil
end
# Summary of the actions taken on this post # Summary of the actions taken on this post
def actions_summary def actions_summary
result = [] result = []
@ -168,7 +189,7 @@ class PostSerializer < BasicPostSerializer
id: id, id: id,
count: count, count: count,
hidden: (sym == :vote), hidden: (sym == :vote),
can_act: scope.post_can_act?(object, sym, taken_actions: post_actions) can_act: scope.post_can_act?(object, sym, taken_actions: actions)
} }
if sym == :notify_user && scope.current_user.present? && scope.current_user == object.user if sym == :notify_user && scope.current_user.present? && scope.current_user == object.user
@ -183,9 +204,9 @@ class PostSerializer < BasicPostSerializer
active_flags[id].count > 0 active_flags[id].count > 0
end end
if post_actions.present? && post_actions.has_key?(id) if actions.present? && actions.has_key?(id)
action_summary[:acted] = true action_summary[:acted] = true
action_summary[:can_undo] = scope.can_delete?(post_actions[id]) action_summary[:can_undo] = scope.can_delete?(actions[id])
end end
# only show public data # only show public data
@ -226,7 +247,7 @@ class PostSerializer < BasicPostSerializer
end end
def include_bookmarked? def include_bookmarked?
post_actions.present? && post_actions.keys.include?(PostActionType.types[:bookmark]) actions.present? && actions.keys.include?(PostActionType.types[:bookmark])
end end
def include_display_username? def include_display_username?

View file

@ -1013,7 +1013,7 @@ en:
disable_edit_notifications: "Disables edit notifications by the system user when 'download_remote_images_to_local' is active." disable_edit_notifications: "Disables edit notifications by the system user when 'download_remote_images_to_local' is active."
enable_names: "Allow showing user full names. Disable to hide full names." enable_names: "Show the user's full name on their profile, user card, and emails. Disable to hide full name everywhere."
display_name_on_posts: "Show a user's full name on their posts in addition to their @username." display_name_on_posts: "Show a user's full name on their posts in addition to their @username."
invites_per_page: "Default invites shown on the user page." invites_per_page: "Default invites shown on the user page."
short_progress_text_threshold: "After the number of posts in a topic goes above this number, the progress bar will only show the current post number. If you change the progress bar's width, you may need to change this value." short_progress_text_threshold: "After the number of posts in a topic goes above this number, the progress bar will only show the current post number. If you change the progress bar's width, you may need to change this value."

View file

@ -269,6 +269,7 @@ Discourse::Application.routes.draw do
get "uploads/:site/:sha" => "uploads#show", constraints: { site: /\w+/, sha: /[a-z0-9]{40}/} get "uploads/:site/:sha" => "uploads#show", constraints: { site: /\w+/, sha: /[a-z0-9]{40}/}
post "uploads" => "uploads#create" post "uploads" => "uploads#create"
get "posts" => "posts#latest"
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"
get "posts/:username/deleted" => "posts#deleted_posts", constraints: {username: USERNAME_ROUTE_FORMAT} get "posts/:username/deleted" => "posts#deleted_posts", constraints: {username: USERNAME_ROUTE_FORMAT}

View file

@ -1,11 +1,13 @@
require 'v8' require 'v8'
require 'nokogiri' require 'nokogiri'
require_dependency 'url_helper'
require_dependency 'excerpt_parser' require_dependency 'excerpt_parser'
require_dependency 'post' require_dependency 'post'
module PrettyText module PrettyText
class Helpers class Helpers
include UrlHelper
def t(key, opts) def t(key, opts)
key = "js." + key key = "js." + key
@ -21,15 +23,15 @@ module PrettyText
# function here are available to v8 # function here are available to v8
def avatar_template(username) def avatar_template(username)
return "" unless username return "" unless username
user = User.find_by(username_lower: username.downcase) user = User.find_by(username_lower: username.downcase)
user.avatar_template if user.present? return "" unless user.present?
schemaless absolute user.avatar_template
end end
def is_username_valid(username) def is_username_valid(username)
return false unless username return false unless username
username = username.downcase username = username.downcase
return User.exec_sql('SELECT 1 FROM users WHERE username_lower = ?', username).values.length == 1 User.exec_sql('SELECT 1 FROM users WHERE username_lower = ?', username).values.length == 1
end end
end end
@ -128,7 +130,9 @@ module PrettyText
context.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};") context.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
context.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';") context.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
context.eval("Discourse.BaseUrl = 'http://#{RailsMultisite::ConnectionManagement.current_hostname}';") context.eval("Discourse.BaseUrl = 'http://#{RailsMultisite::ConnectionManagement.current_hostname}';")
context.eval("Discourse.getURL = function(url) {return '#{Discourse::base_uri}' + url};")
context.eval("Discourse.getURL = function(url) { return '#{Discourse::base_uri}' + url };")
context.eval("Discourse.getURLWithCDN = function(url) { url = Discourse.getURL(url); if (Discourse.CDN) { url = Discourse.CDN + url; } return url; };")
end end
def self.markdown(text, opts=nil) def self.markdown(text, opts=nil)

View file

@ -12,20 +12,20 @@ describe PrettyText do
before(:each) do before(:each) do
eviltrout = User.new eviltrout = User.new
eviltrout.stubs(:avatar_template).returns("http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png") eviltrout.stubs(:avatar_template).returns("//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png")
User.expects(:find_by).with(username_lower: "eviltrout").returns(eviltrout) User.expects(:find_by).with(username_lower: "eviltrout").returns(eviltrout)
end end
it "produces a quote even with new lines in it" do it "produces a quote even with new lines in it" do
expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]")).to match_html "<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\">EvilTrout:</div>\n<blockquote><p>ddd</p></blockquote></aside>" expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]")).to match_html "<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=\"//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout:</div>\n<blockquote><p>ddd</p></blockquote></aside>"
end end
it "should produce a quote" do it "should produce a quote" do
expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]")).to match_html "<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\">EvilTrout:</div>\n<blockquote><p>ddd</p></blockquote></aside>" expect(PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd[/quote]")).to match_html "<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=\"//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout:</div>\n<blockquote><p>ddd</p></blockquote></aside>"
end end
it "trims spaces on quote params" do it "trims spaces on quote params" do
expect(PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]")).to match_html "<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\">EvilTrout:</div>\n<blockquote><p>ddd</p></blockquote></aside>" expect(PrettyText.cook("[quote=\"EvilTrout, post:555, topic: 666\"]ddd[/quote]")).to match_html "<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=\"//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout:</div>\n<blockquote><p>ddd</p></blockquote></aside>"
end end
end end

View file

@ -261,8 +261,14 @@ describe User do
it "downcases email addresses" do it "downcases email addresses" do
user = Fabricate.build(:user, email: 'Fancy.Caps.4.U@gmail.com') user = Fabricate.build(:user, email: 'Fancy.Caps.4.U@gmail.com')
user.save user.valid?
expect(user.reload.email).to eq('fancy.caps.4.u@gmail.com') expect(user.email).to eq('fancy.caps.4.u@gmail.com')
end
it "strips whitespace from email addresses" do
user = Fabricate.build(:user, email: ' example@gmail.com ')
user.valid?
expect(user.email).to eq('example@gmail.com')
end end
end end