Merge branch 'master' of github.com:discourse/discourse
This commit is contained in:
commit
08df4c41cc
|
@ -9,6 +9,6 @@ before_script:
|
||||||
- rake db:migrate
|
- rake db:migrate
|
||||||
- export RUBY_GC_MALLOC_LIMIT=50000000
|
- export RUBY_GC_MALLOC_LIMIT=50000000
|
||||||
bundler_args: --without development
|
bundler_args: --without development
|
||||||
script: 'rake jshint && rake spec && bundle exec guard-jasmine --server-timeout=60'
|
script: 'rake jshint && rake spec && bundle exec rake qunit:test'
|
||||||
services:
|
services:
|
||||||
- redis-server
|
- redis-server
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -103,10 +103,8 @@ group :test, :development do
|
||||||
gem 'certified', require: false
|
gem 'certified', require: false
|
||||||
gem 'fabrication', require: false
|
gem 'fabrication', require: false
|
||||||
gem 'qunit-rails'
|
gem 'qunit-rails'
|
||||||
gem 'guard-jasmine', require: false
|
|
||||||
gem 'guard-rspec', require: false
|
gem 'guard-rspec', require: false
|
||||||
gem 'guard-spork', require: false
|
gem 'guard-spork', require: false
|
||||||
gem 'jasminerice'
|
|
||||||
gem 'mocha', require: false
|
gem 'mocha', require: false
|
||||||
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
|
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
|
||||||
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
|
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
|
||||||
|
@ -118,7 +116,6 @@ group :test, :development do
|
||||||
gem 'rspec-given'
|
gem 'rspec-given'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'pry-nav'
|
gem 'pry-nav'
|
||||||
gem 'webrick'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
|
|
21
Gemfile.lock
21
Gemfile.lock
|
@ -151,13 +151,6 @@ GEM
|
||||||
clockwork (0.5.0)
|
clockwork (0.5.0)
|
||||||
tzinfo (~> 0.3.35)
|
tzinfo (~> 0.3.35)
|
||||||
coderay (1.0.9)
|
coderay (1.0.9)
|
||||||
coffee-rails (3.2.2)
|
|
||||||
coffee-script (>= 2.2.0)
|
|
||||||
railties (~> 3.2.0)
|
|
||||||
coffee-script (2.2.0)
|
|
||||||
coffee-script-source
|
|
||||||
execjs
|
|
||||||
coffee-script-source (1.6.2)
|
|
||||||
connection_pool (1.0.0)
|
connection_pool (1.0.0)
|
||||||
daemons (1.1.9)
|
daemons (1.1.9)
|
||||||
debug_inspector (0.0.2)
|
debug_inspector (0.0.2)
|
||||||
|
@ -206,11 +199,6 @@ GEM
|
||||||
lumberjack (>= 1.0.2)
|
lumberjack (>= 1.0.2)
|
||||||
pry (>= 0.9.10)
|
pry (>= 0.9.10)
|
||||||
thor (>= 0.14.6)
|
thor (>= 0.14.6)
|
||||||
guard-jasmine (1.15.1)
|
|
||||||
childprocess
|
|
||||||
guard (>= 1.1.0)
|
|
||||||
multi_json
|
|
||||||
thor
|
|
||||||
guard-jshint-on-rails (0.0.2)
|
guard-jshint-on-rails (0.0.2)
|
||||||
guard (>= 1.0.0)
|
guard (>= 1.0.0)
|
||||||
jshint_on_rails (>= 1.0.2)
|
jshint_on_rails (>= 1.0.2)
|
||||||
|
@ -221,8 +209,6 @@ GEM
|
||||||
childprocess (>= 0.2.3)
|
childprocess (>= 0.2.3)
|
||||||
guard (>= 1.1)
|
guard (>= 1.1)
|
||||||
spork (>= 0.8.4)
|
spork (>= 0.8.4)
|
||||||
haml (4.0.2)
|
|
||||||
tilt
|
|
||||||
handlebars-source (1.0.0.rc4)
|
handlebars-source (1.0.0.rc4)
|
||||||
has_ip_address (0.0.1)
|
has_ip_address (0.0.1)
|
||||||
hashie (2.0.4)
|
hashie (2.0.4)
|
||||||
|
@ -239,9 +225,6 @@ GEM
|
||||||
image_size (1.1.2)
|
image_size (1.1.2)
|
||||||
image_sorcery (1.1.0)
|
image_sorcery (1.1.0)
|
||||||
in_threads (1.1.1)
|
in_threads (1.1.1)
|
||||||
jasminerice (0.0.10)
|
|
||||||
coffee-rails
|
|
||||||
haml
|
|
||||||
journey (1.0.4)
|
journey (1.0.4)
|
||||||
jshint_on_rails (1.0.2)
|
jshint_on_rails (1.0.2)
|
||||||
json (1.7.7)
|
json (1.7.7)
|
||||||
|
@ -472,7 +455,6 @@ GEM
|
||||||
uglifier (2.0.1)
|
uglifier (2.0.1)
|
||||||
execjs (>= 0.3.0)
|
execjs (>= 0.3.0)
|
||||||
multi_json (~> 1.0, >= 1.0.2)
|
multi_json (~> 1.0, >= 1.0.2)
|
||||||
webrick (1.3.1)
|
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
|
@ -503,7 +485,6 @@ DEPENDENCIES
|
||||||
fast_xs
|
fast_xs
|
||||||
fastimage
|
fastimage
|
||||||
fog
|
fog
|
||||||
guard-jasmine
|
|
||||||
guard-jshint-on-rails
|
guard-jshint-on-rails
|
||||||
guard-rspec
|
guard-rspec
|
||||||
guard-spork
|
guard-spork
|
||||||
|
@ -513,7 +494,6 @@ DEPENDENCIES
|
||||||
hiredis
|
hiredis
|
||||||
image_optim
|
image_optim
|
||||||
image_sorcery
|
image_sorcery
|
||||||
jasminerice
|
|
||||||
jshint_on_rails
|
jshint_on_rails
|
||||||
librarian (>= 0.0.25)
|
librarian (>= 0.0.25)
|
||||||
listen
|
listen
|
||||||
|
@ -573,4 +553,3 @@ DEPENDENCIES
|
||||||
turbo-sprockets-rails3
|
turbo-sprockets-rails3
|
||||||
uglifier
|
uglifier
|
||||||
vestal_versions!
|
vestal_versions!
|
||||||
webrick
|
|
||||||
|
|
17
Guardfile
17
Guardfile
|
@ -3,23 +3,6 @@ require 'terminal-notifier-guard' if RUBY_PLATFORM.include?('darwin')
|
||||||
phantom_path = File.expand_path('~/phantomjs/bin/phantomjs')
|
phantom_path = File.expand_path('~/phantomjs/bin/phantomjs')
|
||||||
phantom_path = nil unless File.exists?(phantom_path)
|
phantom_path = nil unless File.exists?(phantom_path)
|
||||||
|
|
||||||
jasmine_options = {:phantomjs_bin => phantom_path, :server_env => :test}
|
|
||||||
|
|
||||||
if ENV['JASMINE_URL']
|
|
||||||
jasmine_options[:jasmine_url] = ENV['JASMINE_URL']
|
|
||||||
jasmine_options[:server] = :none
|
|
||||||
else
|
|
||||||
jasmine_options[:server] = :thin
|
|
||||||
jasmine_options[:port] = 8888
|
|
||||||
jasmine_options[:server_timeout] = 300
|
|
||||||
end
|
|
||||||
|
|
||||||
guard 'jasmine', jasmine_options do
|
|
||||||
watch(%r{spec/javascripts/spec\.js$}) { "spec/javascripts" }
|
|
||||||
watch(%r{spec/javascripts/.+_spec\.js$})
|
|
||||||
watch(%r{app/assets/javascripts/(.+?)\.js$}) { "spec/javascripts" }
|
|
||||||
end
|
|
||||||
|
|
||||||
# verify that we pass jshint
|
# verify that we pass jshint
|
||||||
# see https://github.com/MrOrz/guard-jshint-on-rails
|
# see https://github.com/MrOrz/guard-jshint-on-rails
|
||||||
guard 'jshint-on-rails', config_path: 'config/jshint.yml' do
|
guard 'jshint-on-rails', config_path: 'config/jshint.yml' do
|
||||||
|
|
|
@ -496,15 +496,12 @@
|
||||||
}
|
}
|
||||||
.topic-meta-data-inside {
|
.topic-meta-data-inside {
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 10px;
|
|
||||||
z-index: 490;
|
z-index: 490;
|
||||||
|
margin-left: 20px;
|
||||||
.post-info {
|
.post-info {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
&:first-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
&.edits {
|
&.edits {
|
||||||
a, a:visited {
|
a, a:visited {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
|
@ -522,7 +519,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
.contents {
|
.contents {
|
||||||
.cooked {
|
.cooked {
|
||||||
padding: 25px 10px 0;
|
padding: 10px 10px 0;
|
||||||
}
|
}
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid #b9b9b9;
|
border: 1px solid #b9b9b9;
|
||||||
|
|
|
@ -15,16 +15,7 @@
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
@include medium-width {
|
|
||||||
h1 {
|
|
||||||
max-width: 735px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@include small-width {
|
|
||||||
h1 {
|
|
||||||
max-width: 690px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.star {
|
.star {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
font-size: 20px !important;
|
font-size: 20px !important;
|
||||||
|
|
|
@ -158,7 +158,7 @@ $muted-important-link-color: #5d5d5d;
|
||||||
// Colors based on basics
|
// Colors based on basics
|
||||||
|
|
||||||
$topicMenuColor: darken($white, 80%);
|
$topicMenuColor: darken($white, 80%);
|
||||||
$bookmarkColor: #b5b500;
|
$bookmarkColor: #0088CC;
|
||||||
|
|
||||||
$tag_color: #e1ecf9;
|
$tag_color: #e1ecf9;
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ require_dependency 'post_destroyer'
|
||||||
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]
|
before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :versions]
|
||||||
|
|
||||||
skip_before_filter :store_incoming_links, only: [:short_link]
|
skip_before_filter :store_incoming_links, only: [:short_link]
|
||||||
skip_before_filter :check_xhr, only: [:markdown,:short_link]
|
skip_before_filter :check_xhr, only: [:markdown,:short_link]
|
||||||
|
|
|
@ -84,7 +84,11 @@ class TopicsController < ApplicationController
|
||||||
raise Discourse::InvalidParameters.new(:title) if title.length < SiteSetting.min_title_similar_length
|
raise Discourse::InvalidParameters.new(:title) if title.length < SiteSetting.min_title_similar_length
|
||||||
raise Discourse::InvalidParameters.new(:raw) if raw.length < SiteSetting.min_body_similar_length
|
raise Discourse::InvalidParameters.new(:raw) if raw.length < SiteSetting.min_body_similar_length
|
||||||
|
|
||||||
topics = Topic.similar_to(title, raw, current_user)
|
# Only suggest similar topics if the site has a minimmum amount of topics present.
|
||||||
|
if Topic.count > SiteSetting.minimum_topics_similar
|
||||||
|
topics = Topic.similar_to(title, raw, current_user)
|
||||||
|
end
|
||||||
|
|
||||||
render_serialized(topics, BasicTopicSerializer)
|
render_serialized(topics, BasicTopicSerializer)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -222,6 +222,8 @@ class SiteSetting < ActiveRecord::Base
|
||||||
client_setting(:topic_views_heat_medium, 2000)
|
client_setting(:topic_views_heat_medium, 2000)
|
||||||
client_setting(:topic_views_heat_high, 5000)
|
client_setting(:topic_views_heat_high, 5000)
|
||||||
|
|
||||||
|
setting(:minimum_topics_similar, 50)
|
||||||
|
|
||||||
def self.generate_api_key!
|
def self.generate_api_key!
|
||||||
self.api_key = SecureRandom.hex(32)
|
self.api_key = SecureRandom.hex(32)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,9 +10,6 @@ Discourse::Application.configure do
|
||||||
# Configure static asset server for tests with Cache-Control for performance
|
# Configure static asset server for tests with Cache-Control for performance
|
||||||
config.serve_static_assets = true
|
config.serve_static_assets = true
|
||||||
|
|
||||||
# Needed for jasmine specs to work
|
|
||||||
config.assets.debug = true
|
|
||||||
|
|
||||||
# Log error messages when you accidentally call methods on nil
|
# Log error messages when you accidentally call methods on nil
|
||||||
config.whiny_nils = true
|
config.whiny_nils = true
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ if defined?(Rack::MiniProfiler)
|
||||||
(env['PATH_INFO'] !~ /^\/message-bus/) &&
|
(env['PATH_INFO'] !~ /^\/message-bus/) &&
|
||||||
(env['PATH_INFO'] !~ /topics\/timings/) &&
|
(env['PATH_INFO'] !~ /topics\/timings/) &&
|
||||||
(env['PATH_INFO'] !~ /assets/) &&
|
(env['PATH_INFO'] !~ /assets/) &&
|
||||||
(env['PATH_INFO'] !~ /jasmine/) &&
|
(env['PATH_INFO'] !~ /qunit/) &&
|
||||||
(env['PATH_INFO'] !~ /users\/.*\/avatar/) &&
|
(env['PATH_INFO'] !~ /users\/.*\/avatar/) &&
|
||||||
(env['PATH_INFO'] !~ /srv\/status/) &&
|
(env['PATH_INFO'] !~ /srv\/status/) &&
|
||||||
(env['PATH_INFO'] !~ /commits-widget/)
|
(env['PATH_INFO'] !~ /commits-widget/)
|
||||||
|
@ -27,7 +27,7 @@ if defined?(Rack::MiniProfiler)
|
||||||
|
|
||||||
# without a user provider our results will use the ip address for namespacing
|
# without a user provider our results will use the ip address for namespacing
|
||||||
# with a load balancer in front this becomes really bad as some results can
|
# with a load balancer in front this becomes really bad as some results can
|
||||||
# be stored associated with ip1 as the user and retrieved using ip2 causing 404s
|
# be stored associated with ip1 as the user and retrieved using ip2 causing 404s
|
||||||
Rack::MiniProfiler.config.user_provider = lambda do |env|
|
Rack::MiniProfiler.config.user_provider = lambda do |env|
|
||||||
request = Rack::Request.new(env)
|
request = Rack::Request.new(env)
|
||||||
id = request.cookies["_t"] || request.ip || "unknown"
|
id = request.cookies["_t"] || request.ip || "unknown"
|
||||||
|
|
|
@ -45,8 +45,8 @@ en:
|
||||||
other: "%{count}y"
|
other: "%{count}y"
|
||||||
medium:
|
medium:
|
||||||
x_minutes:
|
x_minutes:
|
||||||
one: "1 minute"
|
one: "1 min"
|
||||||
other: "%{count} minutes"
|
other: "%{count} mins"
|
||||||
x_hours:
|
x_hours:
|
||||||
one: "1 hour"
|
one: "1 hour"
|
||||||
other: "%{count} hours"
|
other: "%{count} hours"
|
||||||
|
@ -55,8 +55,8 @@ en:
|
||||||
other: "%{count} days"
|
other: "%{count} days"
|
||||||
medium_with_ago:
|
medium_with_ago:
|
||||||
x_minutes:
|
x_minutes:
|
||||||
one: "1 minute ago"
|
one: "1 min ago"
|
||||||
other: "%{count} minutes ago"
|
other: "%{count} mins ago"
|
||||||
x_hours:
|
x_hours:
|
||||||
one: "1 hour ago"
|
one: "1 hour ago"
|
||||||
other: "%{count} hours ago"
|
other: "%{count} hours ago"
|
||||||
|
|
|
@ -383,7 +383,7 @@ en:
|
||||||
memory_warning: 'Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.'
|
memory_warning: 'Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended.'
|
||||||
facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-facebook-logins" target="_blank">See this guide to learn more</a>.'
|
facebook_config_warning: 'The server is configured to allow signup and log in with Facebook (enable_facebook_logins), but the app id and app secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-facebook-logins" target="_blank">See this guide to learn more</a>.'
|
||||||
cas_config_warning: 'The server is configured to allow signup and log in with CAS (enable_cas_logins), but the hostname and domain name values are not set.'
|
cas_config_warning: 'The server is configured to allow signup and log in with CAS (enable_cas_logins), but the hostname and domain name values are not set.'
|
||||||
twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">See this guide to learn more</a>.'
|
twitter_config_warning: 'The server is configured to allow signup and log in with Twitter (enable_twitter_logins), but the key and secret values are not set. Go to <a href="/admin/site_site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">See this guide to learn more</a>.'
|
||||||
github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">See this guide to learn more</a>.'
|
github_config_warning: 'The server is configured to allow signup and log in with GitHub (enable_github_logins), but the client id and secret values are not set. Go to <a href="/admin/site_settings">the Site Settings</a> and update the settings. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">See this guide to learn more</a>.'
|
||||||
failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/environments/production.rb file and ensure that the config.action_mailer settings are correct. <a href="/sidekiq/retries" target="_blank">See the failed jobs in Sidekiq</a>.'
|
failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/environments/production.rb file and ensure that the config.action_mailer settings are correct. <a href="/sidekiq/retries" target="_blank">See the failed jobs in Sidekiq</a>.'
|
||||||
default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the <a href='/admin/site_settings'>Site Settings</a>."
|
default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the <a href='/admin/site_settings'>Site Settings</a>."
|
||||||
|
@ -623,6 +623,8 @@ en:
|
||||||
pop3s_polling_username: "The username for the POP3S account to poll for email"
|
pop3s_polling_username: "The username for the POP3S account to poll for email"
|
||||||
pop3s_polling_password: "The password for the POP3S account to poll for email"
|
pop3s_polling_password: "The password for the POP3S account to poll for email"
|
||||||
|
|
||||||
|
minimum_topics_similar: "How many topics need to exist in the database before similar topics are presented."
|
||||||
|
|
||||||
|
|
||||||
notification_types:
|
notification_types:
|
||||||
mentioned: "%{display_username} mentioned you in %{link}"
|
mentioned: "%{display_username} mentioned you in %{link}"
|
||||||
|
|
|
@ -6,9 +6,16 @@ OS X has become a popular platform for developing Ruby on Rails applications; as
|
||||||
|
|
||||||
Obviously, if you **already** develop Ruby on OS X, a lot of this will be redundant, because you'll have already done it, or something like it. If that's the case, you may well be able to just install Ruby 2.0 using RVM and get started! Discourse has enough dependencies, however (note: not a criticism!) that there's a good chance you'll find **something** else in this document that's useful for getting your Discourse development started!
|
Obviously, if you **already** develop Ruby on OS X, a lot of this will be redundant, because you'll have already done it, or something like it. If that's the case, you may well be able to just install Ruby 2.0 using RVM and get started! Discourse has enough dependencies, however (note: not a criticism!) that there's a good chance you'll find **something** else in this document that's useful for getting your Discourse development started!
|
||||||
|
|
||||||
## Unicode
|
## UTF-8
|
||||||
|
|
||||||
OS X 10.8 uses Unicode by default. You can, of course, double-check this by examining LANG, which appears to be the only relevant environment variable.
|
OS X 10.8 uses UTF-8 by default. You can, of course, double-check this by examining LANG, which appears to be the only relevant environment variable.
|
||||||
|
|
||||||
|
You should see this:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ echo $LANG
|
||||||
|
en_US.UTF
|
||||||
|
```
|
||||||
|
|
||||||
## OSX Development Tools
|
## OSX Development Tools
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,6 @@ The following Ruby Gems are used in Discourse:
|
||||||
* [rspec](https://rubygems.org/gems/rspec)
|
* [rspec](https://rubygems.org/gems/rspec)
|
||||||
* [shoulda](https://rubygems.org/gems/shoulda)
|
* [shoulda](https://rubygems.org/gems/shoulda)
|
||||||
* [turn](https://rubygems.org/gems/turn)
|
* [turn](https://rubygems.org/gems/turn)
|
||||||
* [jasminerice](https://rubygems.org/gems/jasminerice)
|
|
||||||
* [fabrication](https://rubygems.org/gems/fabrication)
|
* [fabrication](https://rubygems.org/gems/fabrication)
|
||||||
* [mocha](https://rubygems.org/gems/mocha)
|
* [mocha](https://rubygems.org/gems/mocha)
|
||||||
* [simplecov](https://rubygems.org/gems/simplecov)
|
* [simplecov](https://rubygems.org/gems/simplecov)
|
||||||
|
|
|
@ -18,13 +18,12 @@ module Email
|
||||||
def process
|
def process
|
||||||
return Email::Receiver.results[:unprocessable] if @raw.blank?
|
return Email::Receiver.results[:unprocessable] if @raw.blank?
|
||||||
|
|
||||||
message = Mail::Message.new(@raw)
|
@message = Mail::Message.new(@raw)
|
||||||
return Email::Receiver.results[:unprocessable] if message.body.blank?
|
parse_body
|
||||||
|
|
||||||
@body = EmailReplyParser.read(message.body.to_s).visible_text
|
|
||||||
return Email::Receiver.results[:unprocessable] if @body.blank?
|
return Email::Receiver.results[:unprocessable] if @body.blank?
|
||||||
|
|
||||||
@reply_key = message.to.first
|
@reply_key = @message.to.first
|
||||||
|
|
||||||
# Extract the `reply_key` from the format the site has specified
|
# Extract the `reply_key` from the format the site has specified
|
||||||
tokens = SiteSetting.reply_by_email_address.split("%{reply_key}")
|
tokens = SiteSetting.reply_by_email_address.split("%{reply_key}")
|
||||||
|
@ -43,6 +42,29 @@ module Email
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def parse_body
|
||||||
|
@body = @message.body.to_s.strip
|
||||||
|
return if @body.blank?
|
||||||
|
|
||||||
|
# I really hate to have to do this, but there seems to be a bug in Mail::Message
|
||||||
|
# with content boundaries in emails. Until it is fixed, this hack removes stuff
|
||||||
|
# we don't want from emails bodies
|
||||||
|
content_type = @message.header['Content-Type'].to_s
|
||||||
|
if content_type.present?
|
||||||
|
boundary_match = content_type.match(/boundary\=(.*)$/)
|
||||||
|
boundary = boundary_match[1] if boundary_match && boundary_match[1].present?
|
||||||
|
if boundary.present? and @body.present?
|
||||||
|
|
||||||
|
lines = @body.lines
|
||||||
|
lines = lines[1..-1] if lines.present? and lines[0] =~ /^--#{boundary}/
|
||||||
|
lines = lines[1..-1] if lines.present? and lines[0] =~ /^Content-Type/
|
||||||
|
|
||||||
|
@body = lines.join.strip!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@body = EmailReplyParser.read(@body).visible_text
|
||||||
|
end
|
||||||
|
|
||||||
def create_reply
|
def create_reply
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ desc "Runs the qunit test suite"
|
||||||
task "qunit:test" => :environment do
|
task "qunit:test" => :environment do
|
||||||
|
|
||||||
require "rack"
|
require "rack"
|
||||||
require "webrick"
|
|
||||||
|
|
||||||
unless %x{which phantomjs > /dev/null 2>&1}
|
unless %x{which phantomjs > /dev/null 2>&1}
|
||||||
abort "PhantomJS is not installed. Download from http://phantomjs.org"
|
abort "PhantomJS is not installed. Download from http://phantomjs.org"
|
||||||
|
@ -12,7 +11,6 @@ task "qunit:test" => :environment do
|
||||||
port = ENV['TEST_SERVER_PORT'] || 60099
|
port = ENV['TEST_SERVER_PORT'] || 60099
|
||||||
server = Thread.new do
|
server = Thread.new do
|
||||||
Rack::Server.start(:config => "config.ru",
|
Rack::Server.start(:config => "config.ru",
|
||||||
:Logger => WEBrick::Log.new("/dev/null"),
|
|
||||||
:AccessLog => [],
|
:AccessLog => [],
|
||||||
:Port => port)
|
:Port => port)
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ module Discourse
|
||||||
MAJOR = 0
|
MAJOR = 0
|
||||||
MINOR = 9
|
MINOR = 9
|
||||||
TINY = 3
|
TINY = 3
|
||||||
PRE = nil
|
PRE = 5
|
||||||
|
|
||||||
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
root = File.expand_path('../../', __FILE__)
|
||||||
|
|
||||||
|
|
||||||
|
puts "Setting up local development environment!"
|
||||||
|
puts
|
||||||
|
|
||||||
|
Dir.chdir root
|
||||||
|
|
||||||
|
puts "Running: bundle"
|
||||||
|
system "bundle"
|
||||||
|
|
||||||
|
|
||||||
|
redis_yml = root + '/config/redis.yml'
|
||||||
|
database_yml = root + '/config/database.yml'
|
||||||
|
|
||||||
|
if !File.exists?(redis_yml)
|
||||||
|
puts "Creating config/redis.yml"
|
||||||
|
system "cp #{root}/config/redis.yml.sample #{redis_yml}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if !File.exists?(database_yml)
|
||||||
|
puts "Creating config/database.yml"
|
||||||
|
system "cp #{root}/config/database.yml.development-sample #{database_yml}"
|
||||||
|
|
||||||
|
puts "Creating development database"
|
||||||
|
system "bundle exec rake db:create"
|
||||||
|
|
||||||
|
puts "Migrating development database"
|
||||||
|
system "bundle exec rake db:migrate"
|
||||||
|
|
||||||
|
puts "Creating test database"
|
||||||
|
system "RAILS_ENV=test bundle exec rake db:create"
|
||||||
|
|
||||||
|
puts "Migrating test database"
|
||||||
|
system "RAILS_ENV=test bundle exec rake db:migrate"
|
||||||
|
end
|
||||||
|
|
||||||
|
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
|
||||||
|
|
||||||
|
if User.count == 0
|
||||||
|
puts "Setting up an admin user"
|
||||||
|
admin = User.new
|
||||||
|
admin.email = "admin@localhost"
|
||||||
|
admin.username = "admin"
|
||||||
|
admin.password = "password"
|
||||||
|
admin.save
|
||||||
|
admin.grant_admin!
|
||||||
|
admin.change_trust_level!(:regular)
|
||||||
|
admin.email_tokens.update_all(confirmed: true)
|
||||||
|
|
||||||
|
puts "An administrator was created:"
|
||||||
|
puts "Username: admin"
|
||||||
|
puts "Password: password"
|
||||||
|
puts
|
||||||
|
puts "To get started run: bundle exec thin start"
|
||||||
|
end
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,17 @@ describe Email::Receiver do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "with a content boundary" do
|
||||||
|
let(:bounded_email) { File.read("#{Rails.root}/spec/fixtures/emails/boundary_email.txt") }
|
||||||
|
let(:receiver) { Email::Receiver.new(bounded_email) }
|
||||||
|
|
||||||
|
it "does something" do
|
||||||
|
receiver.process
|
||||||
|
expect(receiver.body).to eq("I'll look into it, thanks!")
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
describe "with a valid email" do
|
describe "with a valid email" do
|
||||||
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
|
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
|
||||||
let(:valid_reply) { File.read("#{Rails.root}/spec/fixtures/emails/valid_reply.txt") }
|
let(:valid_reply) { File.read("#{Rails.root}/spec/fixtures/emails/valid_reply.txt") }
|
||||||
|
|
|
@ -56,13 +56,7 @@ describe PostsController do
|
||||||
|
|
||||||
describe 'versions' do
|
describe 'versions' do
|
||||||
|
|
||||||
it 'raises an exception when not logged in' do
|
shared_examples 'posts_controller versions examples' do
|
||||||
lambda { xhr :get, :versions, post_id: 123 }.should raise_error(Discourse::NotLoggedIn)
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when logged in' do
|
|
||||||
let(:post) { Fabricate(:post, user: log_in) }
|
|
||||||
|
|
||||||
it "raises an error if the user doesn't have permission to see the post" do
|
it "raises an error if the user doesn't have permission to see the post" do
|
||||||
Guardian.any_instance.expects(:can_see?).with(post).returns(false)
|
Guardian.any_instance.expects(:can_see?).with(post).returns(false)
|
||||||
xhr :get, :versions, post_id: post.id
|
xhr :get, :versions, post_id: post.id
|
||||||
|
@ -73,7 +67,16 @@ describe PostsController do
|
||||||
xhr :get, :versions, post_id: post.id
|
xhr :get, :versions, post_id: post.id
|
||||||
::JSON.parse(response.body).should be_present
|
::JSON.parse(response.body).should be_present
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when not logged in' do
|
||||||
|
let(:post) { Fabricate(:post) }
|
||||||
|
include_examples 'posts_controller versions examples'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when logged in' do
|
||||||
|
let(:post) { Fabricate(:post, user: log_in) }
|
||||||
|
include_examples 'posts_controller versions examples'
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -161,18 +161,40 @@ describe TopicsController do
|
||||||
-> { xhr :get, :similar_to, title: title, raw: raw }.should raise_error(Discourse::InvalidParameters)
|
-> { xhr :get, :similar_to, title: title, raw: raw }.should raise_error(Discourse::InvalidParameters)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "delegates to Topic.similar_to" do
|
describe "minimum_topics_similar" do
|
||||||
Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)])
|
|
||||||
xhr :get, :similar_to, title: title, raw: raw
|
|
||||||
end
|
|
||||||
|
|
||||||
context "logged in" do
|
before do
|
||||||
let(:user) { log_in }
|
SiteSetting.stubs(:minimum_topics_similar).returns(30)
|
||||||
|
end
|
||||||
|
|
||||||
it "passes a user throught if logged in" do
|
after do
|
||||||
Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)])
|
|
||||||
xhr :get, :similar_to, title: title, raw: raw
|
xhr :get, :similar_to, title: title, raw: raw
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "With enough topics" do
|
||||||
|
before do
|
||||||
|
Topic.stubs(:count).returns(50)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "deletes to Topic.similar_to if there are more topics than `minimum_topics_similar`" do
|
||||||
|
Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)])
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "with a logged in user" do
|
||||||
|
let(:user) { log_in }
|
||||||
|
|
||||||
|
it "passes a user through if logged in" do
|
||||||
|
Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
it "does not call Topic.similar_to if there are fewer topics than `minimum_topics_similar`" do
|
||||||
|
Topic.stubs(:count).returns(10)
|
||||||
|
Topic.expects(:similar_to).never
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Received: by 10.64.14.41 with HTTP; Wed, 19 Jun 2013 06:29:41 -0700 (PDT)
|
||||||
|
In-Reply-To: <51c19490e928a_13442dd8ae892548@tree.mail>
|
||||||
|
References: <51c19490e928a_13442dd8ae892548@tree.mail>
|
||||||
|
Date: Wed, 19 Jun 2013 09:29:41 -0400
|
||||||
|
Delivered-To: finn@adventuretime.ooo
|
||||||
|
Message-ID: <CADkmRcL=2aPV7jOcs2i00QGZNwmjYj3qMaJTsKnB7DTP5sgyFQ@mail.gmail.com>
|
||||||
|
Subject: Re: [Adventure Time] jake mentioned you in 'peppermint butler is
|
||||||
|
missing'
|
||||||
|
From: Finn the Human <finn@adventuretime.ooo>
|
||||||
|
To: jake via Adventure Time <math+96dcd9072ba9072d06226009d4223a6e@tree.mail>
|
||||||
|
Content-Type: multipart/alternative; boundary=001a11c206a073876a04df81d2a9
|
||||||
|
|
||||||
|
--001a11c206a073876a04df81d2a9
|
||||||
|
Content-Type: text/plain; charset=ISO-8859-1
|
||||||
|
|
||||||
|
I'll look into it, thanks!
|
||||||
|
|
||||||
|
|
||||||
|
On Wednesday, June 19, 2013, jake via Adventure Time wrote:
|
||||||
|
|
||||||
|
> jake mentioned you in 'peppermint butler is missing' on Adventure
|
||||||
|
> Time:
|
||||||
|
> ------------------------------
|
||||||
|
>
|
||||||
|
> yeah, just noticed this cc @jake
|
||||||
|
> ------------------------------
|
||||||
|
>
|
||||||
|
> Please visit this link to respond:
|
||||||
|
> http://adventuretime.ooo/t/peppermint-butler-is-missing/7628/2
|
||||||
|
>
|
||||||
|
> To unsubscribe from these emails, visit your user preferences
|
||||||
|
> <http://adventuretime.ooo/user_preferences>.
|
||||||
|
>
|
||||||
|
|
||||||
|
--001a11c206a073876a04df81d2a9
|
||||||
|
Content-Type: text/html; charset=ISO-8859-1
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
I'll look into it, thanks!<span></span><br><br>On Wednesday, June 19, 2=
|
||||||
|
013, jake via Adventure Time wrote:<br><blockquote class=3D"gmail_quote" st=
|
||||||
|
yle=3D"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><p>sa=
|
||||||
|
m mentioned you in 'Duplicate message are shown in profile' on Adve=
|
||||||
|
nture Time</p>
|
||||||
|
|
||||||
|
|
||||||
|
<hr><p>yeah, just noticed this cc <a href=3D"http://users/eviltrout" target=
|
||||||
|
=3D"_blank">@eviltrout</a> </p>
|
||||||
|
|
||||||
|
<hr><p>Please visit this link to respond: <a href=3D"http://adventuretime.oo=
|
||||||
|
o/t/duplicate-message-are-shown-in-profile/7628/2" target=3D"_blank">http=
|
||||||
|
://adventuretime.ooo/t/peppermint-butler-is-missing/7628/2</a></=
|
||||||
|
p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>To unsubscribe from these emails, visit your <a href=3D"http://adventuret=
|
||||||
|
ime.ooo/user_preferences" target=3D"_blank">user preferences</a>.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
--001a11c206a073876a04df81d2a9--
|
|
@ -1,221 +0,0 @@
|
||||||
/*global expect:true describe:true it:true beforeEach:true afterEach:true spyOn:true */
|
|
||||||
|
|
||||||
describe("Discourse.ClickTrack", function() {
|
|
||||||
|
|
||||||
var track = Discourse.ClickTrack.trackClick,
|
|
||||||
clickEvent,
|
|
||||||
html = [
|
|
||||||
'<div id="topic" id="1337">',
|
|
||||||
' <article data-post-id="42" data-user-id="3141">',
|
|
||||||
' <a href="http://www.google.com">google.com</a>',
|
|
||||||
' <a class="lightbox back quote-other-topic" href="http://www.google.com">google.com</a>',
|
|
||||||
' <a id="with-badge" data-user-id="314" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
|
||||||
' <a id="with-badge-but-not-mine" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
|
||||||
' <div class="onebox-result">',
|
|
||||||
' <a id="inside-onebox" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
|
||||||
' <a id="inside-onebox-forced" class="track-link" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
|
||||||
' </div>',
|
|
||||||
' <a id="same-site" href="http://discuss.domain.com">forum</a>',
|
|
||||||
' </article>',
|
|
||||||
'</div>'].join("\n");
|
|
||||||
|
|
||||||
var generateClickEventOn = function(selector) {
|
|
||||||
return $.Event("click", { currentTarget: $(selector)[0] });
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
$('body').html(html);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(function() {
|
|
||||||
$('#topic').remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("lightboxes", function() {
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
clickEvent = generateClickEventOn('.lightbox');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not track clicks on lightboxes", function() {
|
|
||||||
expect(track(clickEvent)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not call preventDefault", function() {
|
|
||||||
spyOn(clickEvent, "preventDefault");
|
|
||||||
track(clickEvent);
|
|
||||||
expect(clickEvent.preventDefault).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls preventDefault", function() {
|
|
||||||
clickEvent = generateClickEventOn('a');
|
|
||||||
spyOn(clickEvent, "preventDefault");
|
|
||||||
track(clickEvent);
|
|
||||||
expect(clickEvent.preventDefault).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not track clicks on back buttons", function() {
|
|
||||||
clickEvent = generateClickEventOn('.back');
|
|
||||||
expect(track(clickEvent)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not track clicks on quote buttons", function() {
|
|
||||||
clickEvent = generateClickEventOn('.quote-other-topic');
|
|
||||||
expect(track(clickEvent)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("removes the href and put it as a data attribute", function() {
|
|
||||||
clickEvent = generateClickEventOn('a');
|
|
||||||
track(clickEvent);
|
|
||||||
var $link = $('a').first();
|
|
||||||
expect($link.hasClass('no-href')).toBe(true);
|
|
||||||
expect($link.data('href')).toEqual("http://www.google.com");
|
|
||||||
expect($link.attr('href')).toBeUndefined();
|
|
||||||
expect($link.data('auto-route')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("badges", function() {
|
|
||||||
|
|
||||||
it("does not update badge clicks on my own link", function() {
|
|
||||||
spyOn(Discourse.User, 'current').andReturn(314);
|
|
||||||
spyOn(Discourse, "get").andReturn(314);
|
|
||||||
|
|
||||||
track(generateClickEventOn('#with-badge'));
|
|
||||||
var $badge = $('span.badge', $('#with-badge').first());
|
|
||||||
expect(parseInt($badge.html(), 10)).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not update badge clicks on links in my own post", function() {
|
|
||||||
spyOn(Discourse.User, 'current').andReturn(3141);
|
|
||||||
track(generateClickEventOn('#with-badge-but-not-mine'));
|
|
||||||
var $badge = $('span.badge', $('#with-badge-but-not-mine').first());
|
|
||||||
expect(parseInt($badge.html(), 10)).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("oneboxes", function() {
|
|
||||||
|
|
||||||
it("does not update badge clicks in oneboxes", function() {
|
|
||||||
track(generateClickEventOn('#inside-onebox'));
|
|
||||||
var $badge = $('span.badge', $('#inside-onebox').first());
|
|
||||||
expect(parseInt($badge.html(), 10)).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates badge clicks in oneboxes when forced", function() {
|
|
||||||
track(generateClickEventOn('#inside-onebox-forced'));
|
|
||||||
var $badge = $('span.badge', $('#inside-onebox-forced').first());
|
|
||||||
expect(parseInt($badge.html(), 10)).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updates badge clicks", function() {
|
|
||||||
track(generateClickEventOn('#with-badge'));
|
|
||||||
var $badge = $('span.badge', $('#with-badge').first());
|
|
||||||
expect(parseInt($badge.html(), 10)).toEqual(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("right click", function() {
|
|
||||||
|
|
||||||
beforeEach(function(){
|
|
||||||
clickEvent = generateClickEventOn('a');
|
|
||||||
clickEvent.which = 3;
|
|
||||||
});
|
|
||||||
|
|
||||||
it("detects right clicks", function() {
|
|
||||||
expect(track(clickEvent)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("changes the href", function() {
|
|
||||||
track(clickEvent);
|
|
||||||
var $link = $('a').first();
|
|
||||||
expect($link.attr('href')).toEqual("http://www.google.com");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tracks external right clicks", function() {
|
|
||||||
Discourse.SiteSettings.track_external_right_clicks = true;
|
|
||||||
track(clickEvent);
|
|
||||||
var $link = $('a').first();
|
|
||||||
expect($link.attr('href')).toEqual("/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42");
|
|
||||||
// reset
|
|
||||||
Discourse.SiteSettings.track_external_right_clicks = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("new tab", function() {
|
|
||||||
|
|
||||||
beforeEach(function(){
|
|
||||||
clickEvent = generateClickEventOn('a');
|
|
||||||
spyOn(Discourse, 'ajax');
|
|
||||||
spyOn(window, 'open');
|
|
||||||
});
|
|
||||||
|
|
||||||
var expectItOpensInANewTab = function() {
|
|
||||||
expect(track(clickEvent)).toBe(false);
|
|
||||||
expect(Discourse.ajax).toHaveBeenCalled();
|
|
||||||
expect(window.open).toHaveBeenCalledWith('http://www.google.com', '_blank');
|
|
||||||
};
|
|
||||||
|
|
||||||
it("opens in a new tab when pressing shift", function() {
|
|
||||||
clickEvent.shiftKey = true;
|
|
||||||
expectItOpensInANewTab();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens in a new tab when pressing meta", function() {
|
|
||||||
clickEvent.metaKey = true;
|
|
||||||
expectItOpensInANewTab();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens in a new tab when pressing ctrl", function() {
|
|
||||||
clickEvent.ctrlKey = true;
|
|
||||||
expectItOpensInANewTab();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens in a new tab when middle clicking", function() {
|
|
||||||
clickEvent.which = 2;
|
|
||||||
expectItOpensInANewTab();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it("tracks via AJAX if we're on the same site", function() {
|
|
||||||
// setup
|
|
||||||
clickEvent = generateClickEventOn('#same-site');
|
|
||||||
spyOn(Discourse, 'ajax');
|
|
||||||
spyOn(Discourse.URL, 'routeTo');
|
|
||||||
spyOn(Discourse.URL, 'origin').andReturn('http://discuss.domain.com');
|
|
||||||
// test
|
|
||||||
expect(track(clickEvent)).toBe(false);
|
|
||||||
expect(Discourse.ajax).toHaveBeenCalled();
|
|
||||||
expect(Discourse.URL.routeTo).toHaveBeenCalledWith('http://discuss.domain.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("tracks via custom URL", function() {
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
clickEvent = generateClickEventOn('a');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("in another window", function() {
|
|
||||||
// spies
|
|
||||||
spyOn(Discourse.User, 'current').andReturn(true);
|
|
||||||
spyOn(window, 'open').andCallFake(function() { return { focus: function() {} } });
|
|
||||||
spyOn(window, 'focus');
|
|
||||||
// test
|
|
||||||
expect(track(clickEvent)).toBe(false);
|
|
||||||
expect(window.open).toHaveBeenCalledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42', '_blank');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("in the same window", function() {
|
|
||||||
spyOn(Discourse.URL, 'redirectTo');
|
|
||||||
expect(track(clickEvent)).toBe(false);
|
|
||||||
expect(Discourse.URL.redirectTo).toHaveBeenCalledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,63 +0,0 @@
|
||||||
/*global waitsFor:true expect:true describe:true beforeEach:true it:true spyOn:true */
|
|
||||||
|
|
||||||
describe("Discourse.Utilities", function() {
|
|
||||||
|
|
||||||
describe("emailValid", function() {
|
|
||||||
|
|
||||||
it("allows upper case in first part of emails", function() {
|
|
||||||
expect(Discourse.Utilities.emailValid('Bob@example.com')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("allows upper case in domain of emails", function() {
|
|
||||||
expect(Discourse.Utilities.emailValid('bob@EXAMPLE.com')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("validateFilesForUpload", function() {
|
|
||||||
|
|
||||||
it("returns false when file is undefined", function() {
|
|
||||||
expect(Discourse.Utilities.validateFilesForUpload(null)).toBe(false);
|
|
||||||
expect(Discourse.Utilities.validateFilesForUpload(undefined)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns false when file there is no file", function() {
|
|
||||||
expect(Discourse.Utilities.validateFilesForUpload([])).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("supports only one file", function() {
|
|
||||||
spyOn(bootbox, 'alert');
|
|
||||||
spyOn(Em.String, 'i18n');
|
|
||||||
expect(Discourse.Utilities.validateFilesForUpload([1, 2])).toBe(false);
|
|
||||||
expect(bootbox.alert).toHaveBeenCalled();
|
|
||||||
expect(Em.String.i18n).toHaveBeenCalledWith('post.errors.upload_too_many_images');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("supports only an image", function() {
|
|
||||||
var html = { type: "text/html" };
|
|
||||||
spyOn(bootbox, 'alert');
|
|
||||||
spyOn(Em.String, 'i18n');
|
|
||||||
expect(Discourse.Utilities.validateFilesForUpload([html])).toBe(false);
|
|
||||||
expect(bootbox.alert).toHaveBeenCalled();
|
|
||||||
expect(Em.String.i18n).toHaveBeenCalledWith('post.errors.only_images_are_supported');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prevents the upload of a too large image", function() {
|
|
||||||
var image = { type: "image/png", size: 10 * 1024 };
|
|
||||||
Discourse.SiteSettings.max_upload_size_kb = 5;
|
|
||||||
spyOn(bootbox, 'alert');
|
|
||||||
spyOn(Em.String, 'i18n');
|
|
||||||
expect(Discourse.Utilities.validateFilesForUpload([image])).toBe(false);
|
|
||||||
expect(bootbox.alert).toHaveBeenCalled();
|
|
||||||
expect(Em.String.i18n).toHaveBeenCalledWith('post.errors.upload_too_large', { max_size_kb: 5 });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("works", function() {
|
|
||||||
var image = { type: "image/png", size: 10 * 1024 };
|
|
||||||
Discourse.SiteSettings.max_upload_size_kb = 15;
|
|
||||||
expect(Discourse.Utilities.validateFilesForUpload([image])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,21 +0,0 @@
|
||||||
// hacks for ember, this sets up our app for testing
|
|
||||||
|
|
||||||
(function(){
|
|
||||||
|
|
||||||
var currentWindowOnload = window.onload;
|
|
||||||
window.onload = function() {
|
|
||||||
if (currentWindowOnload) {
|
|
||||||
currentWindowOnload();
|
|
||||||
}
|
|
||||||
|
|
||||||
$('<div id="main"><div class="rootElement"></div></div>').appendTo($('body')).hide();
|
|
||||||
|
|
||||||
Discourse.SiteSettings = {}
|
|
||||||
|
|
||||||
Discourse.Router.map(function() {
|
|
||||||
this.route("jasmine",{path: "/jasmine"});
|
|
||||||
Discourse.routeBuilder.apply(this)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
})()
|
|
|
@ -1,98 +0,0 @@
|
||||||
/*global waitsFor:true expect:true describe:true beforeEach:true it:true */
|
|
||||||
|
|
||||||
describe("Discourse.Report", function() {
|
|
||||||
|
|
||||||
function dateString(days) {
|
|
||||||
return moment().subtract("days", days).format('YYYY-MM-DD');
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportWithData(data) {
|
|
||||||
var arr = [];
|
|
||||||
_.each(data,function(val,index) {
|
|
||||||
arr.push({x: dateString(index), y: val});
|
|
||||||
});
|
|
||||||
return Discourse.Report.create({ type: 'topics', data: arr });
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("todayCount", function() {
|
|
||||||
it("returns the correct value", function() {
|
|
||||||
expect( reportWithData([5,4,3,2,1]).get('todayCount') ).toBe(5);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("yesterdayCount", function() {
|
|
||||||
it("returns the correct value", function() {
|
|
||||||
expect( reportWithData([5,4,3,2,1]).get('yesterdayCount') ).toBe(4);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sumDays", function() {
|
|
||||||
it("adds the values for the given range of days, inclusive", function() {
|
|
||||||
expect( reportWithData([1,2,3,5,8,13]).sumDays(2,4) ).toBe(16);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("lastSevenDaysCount", function() {
|
|
||||||
it("returns the correct value", function() {
|
|
||||||
expect( reportWithData([100,9,8,7,6,5,4,3,200,300,400]).get('lastSevenDaysCount') ).toBe(42);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("percentChangeString", function() {
|
|
||||||
it("returns correct value when value increased", function() {
|
|
||||||
expect( reportWithData([]).percentChangeString(8,5) ).toBe("+60%");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns correct value when value decreased", function() {
|
|
||||||
expect( reportWithData([]).percentChangeString(2,8) ).toBe("-75%");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns 0 when value is unchanged", function() {
|
|
||||||
expect( reportWithData([]).percentChangeString(8,8) ).toBe("0%");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns Infinity when previous value was 0", function() {
|
|
||||||
expect( reportWithData([]).percentChangeString(8,0) ).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns -100 when yesterday's value was 0", function() {
|
|
||||||
expect( reportWithData([]).percentChangeString(0,8) ).toBe('-100%');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns NaN when both yesterday and the previous day were both 0", function() {
|
|
||||||
expect( reportWithData([]).percentChangeString(0,0) ).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("yesterdayCountTitle", function() {
|
|
||||||
it("displays percent change and previous value", function(){
|
|
||||||
var title = reportWithData([6,8,5,2,1]).get('yesterdayCountTitle')
|
|
||||||
expect( title.indexOf('+60%') ).not.toBe(-1);
|
|
||||||
expect( title ).toMatch("Was 5");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles when two days ago was 0", function() {
|
|
||||||
var title = reportWithData([6,8,0,2,1]).get('yesterdayCountTitle')
|
|
||||||
expect( title ).toMatch("Was 0");
|
|
||||||
expect( title ).not.toMatch("%");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sevenDayCountTitle", function() {
|
|
||||||
it("displays percent change and previous value", function(){
|
|
||||||
var title = reportWithData([100,1,1,1,1,1,1,1,2,2,2,2,2,2,2,100,100]).get('sevenDayCountTitle');
|
|
||||||
expect( title ).toMatch("-50%");
|
|
||||||
expect( title ).toMatch("Was 14");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("thirtyDayCountTitle", function() {
|
|
||||||
it("displays percent change and previous value", function(){
|
|
||||||
var report = reportWithData([5,5,5,5]);
|
|
||||||
report.set('prev30Days', 10);
|
|
||||||
var title = report.get('thirtyDayCountTitle');
|
|
||||||
expect( title.indexOf('+50%') ).not.toBe(-1);
|
|
||||||
expect( title ).toMatch("Was 10");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
/*global waitsFor:true expect:true describe:true beforeEach:true it:true */
|
|
||||||
|
|
||||||
describe("Discourse.UserAction", function() {
|
|
||||||
|
|
||||||
describe("collapseStream", function() {
|
|
||||||
|
|
||||||
it("collapses all likes", function() {
|
|
||||||
var actions = [
|
|
||||||
Discourse.UserAction.create({
|
|
||||||
action_type: Discourse.UserAction.LIKE,
|
|
||||||
topic_id: 1,
|
|
||||||
user_id: 1,
|
|
||||||
post_number: 1
|
|
||||||
}), Discourse.UserAction.create({
|
|
||||||
action_type: Discourse.UserAction.EDIT,
|
|
||||||
topic_id: 2,
|
|
||||||
user_id: 1,
|
|
||||||
post_number: 1
|
|
||||||
}), Discourse.UserAction.create({
|
|
||||||
action_type: Discourse.UserAction.LIKE,
|
|
||||||
topic_id: 1,
|
|
||||||
user_id: 2,
|
|
||||||
post_number: 1
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
actions = Discourse.UserAction.collapseStream(actions);
|
|
||||||
|
|
||||||
expect(actions.length).toBe(2);
|
|
||||||
expect(actions[0].get("children").length).toBe(1);
|
|
||||||
expect(actions[0].get("children")[0].items.length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,106 +0,0 @@
|
||||||
/*global waitsFor:true expect:true describe:true beforeEach:true it:true runs:true */
|
|
||||||
|
|
||||||
describe("PreloadStore", function() {
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
PreloadStore.store('bane', 'evil');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('get', function() {
|
|
||||||
|
|
||||||
it("returns undefined if the key doesn't exist", function() {
|
|
||||||
expect(PreloadStore.get('joker')).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns the value if the key exists", function() {
|
|
||||||
expect(PreloadStore.get('bane')).toBe('evil');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('remove', function() {
|
|
||||||
|
|
||||||
it("removes the value if the key exists", function() {
|
|
||||||
PreloadStore.remove('bane');
|
|
||||||
expect(PreloadStore.get('bane')).toBe(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAndRemove', function() {
|
|
||||||
|
|
||||||
it("returns a promise that resolves to null", function() {
|
|
||||||
var done, storeResult;
|
|
||||||
done = storeResult = null;
|
|
||||||
PreloadStore.getAndRemove('joker').then(function(result) {
|
|
||||||
done = true;
|
|
||||||
storeResult = result;
|
|
||||||
});
|
|
||||||
waitsFor((function() { return done; }), "Promise never resolved", 1000);
|
|
||||||
runs(function() {
|
|
||||||
expect(storeResult).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a promise that resolves to the result of the finder", function() {
|
|
||||||
var done, finder, storeResult;
|
|
||||||
done = storeResult = null;
|
|
||||||
finder = function() { return 'evil'; };
|
|
||||||
PreloadStore.getAndRemove('joker', finder).then(function(result) {
|
|
||||||
done = true;
|
|
||||||
storeResult = result;
|
|
||||||
});
|
|
||||||
waitsFor((function() { return done; }), "Promise never resolved", 1000);
|
|
||||||
runs(function() {
|
|
||||||
expect(storeResult).toBe('evil');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a promise that resolves to the result of the finder's promise", function() {
|
|
||||||
var done, finder, storeResult;
|
|
||||||
done = storeResult = null;
|
|
||||||
finder = function() {
|
|
||||||
return Ember.Deferred.promise(function(promise) { promise.resolve('evil'); });
|
|
||||||
};
|
|
||||||
PreloadStore.getAndRemove('joker', finder).then(function(result) {
|
|
||||||
done = true;
|
|
||||||
storeResult = result;
|
|
||||||
});
|
|
||||||
waitsFor((function() { return done; }), "Promise never resolved", 1000);
|
|
||||||
runs(function() {
|
|
||||||
expect(storeResult).toBe('evil');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a promise that resolves to the result of the finder's rejected promise", function() {
|
|
||||||
var done, finder, storeResult;
|
|
||||||
done = storeResult = null;
|
|
||||||
finder = function() {
|
|
||||||
return Ember.Deferred.promise(function(promise) { promise.reject('evil'); });
|
|
||||||
};
|
|
||||||
PreloadStore.getAndRemove('joker', finder).then(null, function(rejectedResult) {
|
|
||||||
done = true;
|
|
||||||
storeResult = rejectedResult;
|
|
||||||
});
|
|
||||||
waitsFor((function() { return done; }), "Promise never rejected", 1000);
|
|
||||||
runs(function() {
|
|
||||||
expect(storeResult).toBe('evil');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns a promise that resolves to 'evil'", function() {
|
|
||||||
var done, storeResult;
|
|
||||||
done = storeResult = null;
|
|
||||||
PreloadStore.getAndRemove('bane').then(function(result) {
|
|
||||||
done = true;
|
|
||||||
storeResult = result;
|
|
||||||
});
|
|
||||||
waitsFor((function() { return done; }), "Promise never resolved", 1000);
|
|
||||||
runs(function() {
|
|
||||||
expect(storeResult).toBe('evil');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,15 +0,0 @@
|
||||||
/*global waitsFor:true expect:true describe:true beforeEach:true it:true sanitizeHtml:true */
|
|
||||||
|
|
||||||
describe("sanitize", function(){
|
|
||||||
|
|
||||||
it("strips all script tags", function(){
|
|
||||||
var sanitized = sanitizeHtml("<div><script>alert('hi');</script></div>");
|
|
||||||
expect(sanitized).toBe("<div></div>");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("strips disallowed attributes", function(){
|
|
||||||
var sanitized = sanitizeHtml("<div><p class=\"funky\" wrong='1'>hello</p></div>");
|
|
||||||
expect(sanitized).toBe("<div><p class=\"funky\">hello</p></div>");
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
|
@ -1,3 +0,0 @@
|
||||||
/*
|
|
||||||
|
|
||||||
*/
|
|
|
@ -1,51 +0,0 @@
|
||||||
|
|
||||||
//= require env
|
|
||||||
|
|
||||||
//= require ../../app/assets/javascripts/preload_store.js
|
|
||||||
|
|
||||||
// probe framework first
|
|
||||||
//= require ../../app/assets/javascripts/discourse/components/probes.js
|
|
||||||
|
|
||||||
// Externals we need to load first
|
|
||||||
//= require ../../app/assets/javascripts/external/jquery-1.9.1.js
|
|
||||||
|
|
||||||
//= require ../../app/assets/javascripts/external/jquery.ui.widget.js
|
|
||||||
//= require ../../app/assets/javascripts/external/handlebars-1.0.rc.4.js
|
|
||||||
//= require ../../app/assets/javascripts/external_production/ember.js
|
|
||||||
//= require ../../app/assets/javascripts/external_production/group-helper.js
|
|
||||||
|
|
||||||
//= require ../../app/assets/javascripts/locales/i18n
|
|
||||||
//= require ../../app/assets/javascripts/locales/date_locales.js
|
|
||||||
//= require ../../app/assets/javascripts/discourse/helpers/i18n_helpers
|
|
||||||
//= require ../../app/assets/javascripts/locales/en
|
|
||||||
//
|
|
||||||
// Pagedown customizations
|
|
||||||
//= require ../../app/assets/javascripts/pagedown_custom.js
|
|
||||||
|
|
||||||
// The rest of the externals
|
|
||||||
//= require_tree ../../app/assets/javascripts/external
|
|
||||||
|
|
||||||
//= require ../../app/assets/javascripts/discourse
|
|
||||||
|
|
||||||
// Stuff we need to load first
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/mixins
|
|
||||||
//= require ../../app/assets/javascripts/discourse/components/debounce
|
|
||||||
//= require ../../app/assets/javascripts/discourse/controllers/controller
|
|
||||||
//= require ../../app/assets/javascripts/discourse/views/modal/modal_body_view
|
|
||||||
//= require ../../app/assets/javascripts/discourse/models/model
|
|
||||||
//= require ../../app/assets/javascripts/discourse/routes/discourse_route
|
|
||||||
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/controllers
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/components
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/models
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/views
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/helpers
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/templates
|
|
||||||
//= require_tree ../../app/assets/javascripts/discourse/routes
|
|
||||||
//= require_tree ../../app/assets/javascripts/admin/models
|
|
||||||
|
|
||||||
//= require_tree ../../app/assets/javascripts/defer
|
|
||||||
|
|
||||||
//= require_tree .
|
|
||||||
|
|
||||||
//= require hacks
|
|
|
@ -0,0 +1,173 @@
|
||||||
|
/*global module:true test:true ok:true visit:true equal:true exists:true count:true equal:true present:true sinon:true blank:true */
|
||||||
|
|
||||||
|
module("Discourse.ClickTrack", {
|
||||||
|
setup: function() {
|
||||||
|
|
||||||
|
// Prevent any of these tests from navigating away
|
||||||
|
this.win = {focus: function() { } };
|
||||||
|
this.redirectTo = sinon.stub(Discourse.URL, "redirectTo");
|
||||||
|
sinon.stub(Discourse, "ajax");
|
||||||
|
this.windowOpen = sinon.stub(window, "open").returns(this.win);
|
||||||
|
sinon.stub(this.win, "focus");
|
||||||
|
|
||||||
|
$('#qunit-scratch').html([
|
||||||
|
'<div id="topic" id="1337">',
|
||||||
|
' <article data-post-id="42" data-user-id="3141">',
|
||||||
|
' <a href="http://www.google.com">google.com</a>',
|
||||||
|
' <a class="lightbox back quote-other-topic" href="http://www.google.com">google.com</a>',
|
||||||
|
' <a id="with-badge" data-user-id="314" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||||
|
' <a id="with-badge-but-not-mine" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||||
|
' <div class="onebox-result">',
|
||||||
|
' <a id="inside-onebox" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||||
|
' <a id="inside-onebox-forced" class="track-link" href="http://www.google.com">google.com<span class="badge">1</span></a>',
|
||||||
|
' </div>',
|
||||||
|
' <a id="same-site" href="http://discuss.domain.com">forum</a>',
|
||||||
|
' </article>',
|
||||||
|
'</div>'].join("\n"));
|
||||||
|
},
|
||||||
|
|
||||||
|
teardown: function() {
|
||||||
|
$('#topic').remove();
|
||||||
|
$('#qunit-scratch').html('');
|
||||||
|
|
||||||
|
Discourse.URL.redirectTo.restore();
|
||||||
|
Discourse.ajax.restore();
|
||||||
|
window.open.restore();
|
||||||
|
this.win.focus.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var track = Discourse.ClickTrack.trackClick;
|
||||||
|
|
||||||
|
var generateClickEventOn = function(selector) {
|
||||||
|
return $.Event("click", { currentTarget: $(selector)[0] });
|
||||||
|
}
|
||||||
|
|
||||||
|
test("does not track clicks on lightboxes", function() {
|
||||||
|
var clickEvent = generateClickEventOn('.lightbox');
|
||||||
|
this.stub(clickEvent, "preventDefault");
|
||||||
|
ok(track(clickEvent));
|
||||||
|
ok(!clickEvent.preventDefault.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it calls preventDefault when clicking on an a", function() {
|
||||||
|
var clickEvent = generateClickEventOn('a');
|
||||||
|
this.stub(clickEvent, "preventDefault");
|
||||||
|
track(clickEvent);
|
||||||
|
ok(clickEvent.preventDefault.calledOnce);
|
||||||
|
ok(Discourse.URL.redirectTo.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not track clicks on back buttons", function() {
|
||||||
|
ok(track(generateClickEventOn('.back')))
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not track clicks on quote buttons", function() {
|
||||||
|
ok(track(generateClickEventOn('.quote-other-topic')))
|
||||||
|
});
|
||||||
|
|
||||||
|
test("removes the href and put it as a data attribute", function() {
|
||||||
|
track(generateClickEventOn('a'));
|
||||||
|
|
||||||
|
var $link = $('a').first();
|
||||||
|
ok($link.hasClass('no-href'));
|
||||||
|
equal($link.data('href'), 'http://www.google.com');
|
||||||
|
blank($link.attr('href'));
|
||||||
|
ok($link.data('auto-route'));
|
||||||
|
ok(Discourse.URL.redirectTo.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var badgeClickCount = function(id, expected) {
|
||||||
|
track(generateClickEventOn('#' + id));
|
||||||
|
var $badge = $('span.badge', $('#' + id).first());
|
||||||
|
equal(parseInt($badge.html(), 10), expected);
|
||||||
|
};
|
||||||
|
|
||||||
|
test("does not update badge clicks on my own link", function() {
|
||||||
|
this.stub(Discourse.User, 'current').returns(314);
|
||||||
|
badgeClickCount('with-badge', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not update badge clicks in my own post", function() {
|
||||||
|
this.stub(Discourse.User, 'current').returns(3141);
|
||||||
|
badgeClickCount('with-badge-but-not-mine', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("updates badge counts correctly", function() {
|
||||||
|
badgeClickCount('inside-onebox', 1);
|
||||||
|
badgeClickCount('inside-onebox-forced', 2);
|
||||||
|
badgeClickCount('with-badge', 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
var trackRightClick = function() {
|
||||||
|
var clickEvent = generateClickEventOn('a')
|
||||||
|
clickEvent.which = 3;
|
||||||
|
return track(clickEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
test("right clicks change the href", function() {
|
||||||
|
ok(trackRightClick());
|
||||||
|
equal($('a').first().prop('href'), "http://www.google.com/");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("right clicks are tracked", function() {
|
||||||
|
Discourse.SiteSettings.track_external_right_clicks = true;
|
||||||
|
trackRightClick();
|
||||||
|
equal($('a').first().attr('href'), "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42");
|
||||||
|
Discourse.SiteSettings.track_external_right_clicks = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var expectToOpenInANewTab = function(clickEvent) {
|
||||||
|
ok(!track(clickEvent));
|
||||||
|
ok(Discourse.ajax.calledOnce);
|
||||||
|
ok(window.open.calledOnce);
|
||||||
|
};
|
||||||
|
|
||||||
|
test("it opens in a new tab when pressing shift", function() {
|
||||||
|
var clickEvent = generateClickEventOn('a');
|
||||||
|
clickEvent.shiftKey = true;
|
||||||
|
expectToOpenInANewTab(clickEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it opens in a new tab when pressing meta", function() {
|
||||||
|
var clickEvent = generateClickEventOn('a');
|
||||||
|
clickEvent.metaKey = true;
|
||||||
|
expectToOpenInANewTab(clickEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it opens in a new tab when pressing meta", function() {
|
||||||
|
var clickEvent = generateClickEventOn('a');
|
||||||
|
clickEvent.ctrlKey = true;
|
||||||
|
expectToOpenInANewTab(clickEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it opens in a new tab when pressing meta", function() {
|
||||||
|
var clickEvent = generateClickEventOn('a');
|
||||||
|
clickEvent.which = 2;
|
||||||
|
expectToOpenInANewTab(clickEvent);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tracks via AJAX if we're on the same site", function() {
|
||||||
|
this.stub(Discourse.URL, "routeTo");
|
||||||
|
this.stub(Discourse.URL, "origin").returns("http://discuss.domain.com");
|
||||||
|
|
||||||
|
ok(!track(generateClickEventOn('#same-site')));
|
||||||
|
ok(Discourse.ajax.calledOnce);
|
||||||
|
ok(Discourse.URL.routeTo.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("tracks custom urls when opening in another window", function() {
|
||||||
|
var clickEvent = generateClickEventOn('a');
|
||||||
|
this.stub(Discourse.User, "current").returns(true);
|
||||||
|
ok(!track(clickEvent));
|
||||||
|
ok(this.windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42', '_blank'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("tracks custom urls when opening in another window", function() {
|
||||||
|
var clickEvent = generateClickEventOn('a');
|
||||||
|
ok(!track(clickEvent));
|
||||||
|
ok(this.redirectTo.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42'));
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
/*global module:true test:true ok:true visit:true equal:true exists:true count:true equal:true present:true md5:true */
|
/*global module:true test:true ok:true visit:true equal:true exists:true count:true equal:true present:true md5:true sanitizeHtml:true */
|
||||||
|
|
||||||
module("Discourse.Markdown");
|
module("Discourse.Markdown");
|
||||||
|
|
||||||
|
@ -93,3 +93,9 @@ test("Oneboxing", function() {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("SanitizeHTML", function() {
|
||||||
|
|
||||||
|
equal(sanitizeHtml("<div><script>alert('hi');</script></div>"), "<div></div>");
|
||||||
|
equal(sanitizeHtml("<div><p class=\"funky\" wrong='1'>hello</p></div>"), "<div><p class=\"funky\">hello</p></div>");
|
||||||
|
|
||||||
|
});
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*global module:true test:true ok:true visit:true expect:true exists:true equal:true count:true present:true asyncTest:true blank:true */
|
||||||
|
|
||||||
|
module("Discourse.PreloadStore", {
|
||||||
|
setup: function() {
|
||||||
|
PreloadStore.store('bane', 'evil');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("get", function() {
|
||||||
|
blank(PreloadStore.get('joker'), "returns blank for a missing key");
|
||||||
|
equal(PreloadStore.get('bane'), 'evil', "returns the value for that key");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("remove", function() {
|
||||||
|
PreloadStore.remove('bane');
|
||||||
|
blank(PreloadStore.get('bane'), "removes the value if the key exists");
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest("getAndRemove returns a promise that resolves to null", function() {
|
||||||
|
expect(1);
|
||||||
|
|
||||||
|
PreloadStore.getAndRemove('joker').then(function(result) {
|
||||||
|
blank(result);
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest("getAndRemove returns a promise that resolves to the result of the finder", function() {
|
||||||
|
expect(1);
|
||||||
|
|
||||||
|
var finder = function() { return 'batdance'; };
|
||||||
|
PreloadStore.getAndRemove('joker', finder).then(function(result) {
|
||||||
|
equal(result, 'batdance');
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest("getAndRemove returns a promise that resolves to the result of the finder's promise", function() {
|
||||||
|
expect(1);
|
||||||
|
|
||||||
|
var finder = function() {
|
||||||
|
return Ember.Deferred.promise(function(promise) { promise.resolve('hahahah'); });
|
||||||
|
};
|
||||||
|
|
||||||
|
PreloadStore.getAndRemove('joker', finder).then(function(result) {
|
||||||
|
equal(result, 'hahahah');
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest("returns a promise that rejects with the result of the finder's rejected promise", function() {
|
||||||
|
expect(1);
|
||||||
|
|
||||||
|
var finder = function() {
|
||||||
|
return Ember.Deferred.promise(function(promise) { promise.reject('error'); });
|
||||||
|
};
|
||||||
|
|
||||||
|
PreloadStore.getAndRemove('joker', finder).then(null, function(result) {
|
||||||
|
equal(result, 'error');
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
asyncTest("returns a promise that resolves to 'evil'", function() {
|
||||||
|
expect(1);
|
||||||
|
|
||||||
|
PreloadStore.getAndRemove('bane').then(function(result) {
|
||||||
|
equal(result, 'evil');
|
||||||
|
start();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*global module:true test:true ok:true visit:true expect:true exists:true count:true equal:true */
|
||||||
|
|
||||||
|
module("Discourse.Utilities");
|
||||||
|
|
||||||
|
var utils = Discourse.Utilities;
|
||||||
|
|
||||||
|
test("emailValid", function() {
|
||||||
|
ok(utils.emailValid('Bob@example.com'), "allows upper case in the first part of emails");
|
||||||
|
ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var validUpload = utils.validateFilesForUpload;
|
||||||
|
|
||||||
|
test("validateFilesForUpload", function() {
|
||||||
|
ok(!validUpload(null), "no files are invalid");
|
||||||
|
ok(!validUpload(undefined), "undefined files are invalid");
|
||||||
|
ok(!validUpload([]), "empty array of files is invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uploading one file", function() {
|
||||||
|
this.stub(bootbox, "alert");
|
||||||
|
|
||||||
|
ok(!validUpload([1, 2]));
|
||||||
|
ok(bootbox.alert.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ensures an image upload", function() {
|
||||||
|
var html = { type: "text/html" };
|
||||||
|
this.stub(bootbox, "alert");
|
||||||
|
|
||||||
|
ok(!validUpload([html]));
|
||||||
|
ok(bootbox.alert.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prevents files that are too big from being uploaded", function() {
|
||||||
|
var image = { type: "image/png", size: 10 * 1024 };
|
||||||
|
Discourse.SiteSettings.max_upload_size_kb = 5;
|
||||||
|
this.stub(bootbox, "alert");
|
||||||
|
|
||||||
|
ok(!validUpload([image]));
|
||||||
|
ok(bootbox.alert.calledOnce);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("allows valid uploads to go through", function() {
|
||||||
|
var image = { type: "image/png", size: 10 * 1024 };
|
||||||
|
Discourse.SiteSettings.max_upload_size_kb = 15;
|
||||||
|
this.stub(bootbox, "alert");
|
||||||
|
|
||||||
|
ok(validUpload([image]));
|
||||||
|
ok(!bootbox.alert.calledOnce);
|
||||||
|
});
|
|
@ -13,6 +13,8 @@ module("Header", {
|
||||||
test("/", function() {
|
test("/", function() {
|
||||||
|
|
||||||
visit("/").then(function() {
|
visit("/").then(function() {
|
||||||
|
expect(2);
|
||||||
|
|
||||||
ok(exists("header"), "The header was rendered");
|
ok(exists("header"), "The header was rendered");
|
||||||
ok(exists("#site-logo"), "The logo was shown");
|
ok(exists("#site-logo"), "The logo was shown");
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,8 @@ module("List Topics", {
|
||||||
test("/", function() {
|
test("/", function() {
|
||||||
|
|
||||||
visit("/").then(function() {
|
visit("/").then(function() {
|
||||||
|
expect(2);
|
||||||
|
|
||||||
ok(exists("#topic-list"), "The list of topics was rendered");
|
ok(exists("#topic-list"), "The list of topics was rendered");
|
||||||
ok(count('#topic-list .topic-list-item') > 0, "has topics");
|
ok(count('#topic-list .topic-list-item') > 0, "has topics");
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*global module:true test:true ok:true visit:true expect:true exists:true count:true equal:true blank:true */
|
||||||
|
|
||||||
|
module("Discourse.Report");
|
||||||
|
|
||||||
|
function reportWithData(data) {
|
||||||
|
return Discourse.Report.create({
|
||||||
|
type: 'topics',
|
||||||
|
data: _.map(data, function(val, index) {
|
||||||
|
return {x: moment().subtract("days", index).format('YYYY-MM-DD'), y: val};
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("counts", function() {
|
||||||
|
var report = reportWithData([5, 4, 3, 2, 1, 100, 99, 98, 1000]);
|
||||||
|
|
||||||
|
equal(report.get('todayCount'), 5);
|
||||||
|
equal(report.get('yesterdayCount'), 4);
|
||||||
|
equal(report.sumDays(2, 4), 6, "adds the values for the given range of days, inclusive");
|
||||||
|
equal(report.get('lastSevenDaysCount'), 307, "sums 7 days excluding today");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("percentChangeString", function() {
|
||||||
|
var report = reportWithData([]);
|
||||||
|
|
||||||
|
equal(report.percentChangeString(8, 5), "+60%", "value increased");
|
||||||
|
equal(report.percentChangeString(2, 8), "-75%", "value decreased");
|
||||||
|
equal(report.percentChangeString(8, 8), "0%", "value unchanged");
|
||||||
|
blank(report.percentChangeString(8, 0), "returns blank when previous value was 0");
|
||||||
|
equal(report.percentChangeString(0, 8), "-100%", "yesterday was 0");
|
||||||
|
blank(report.percentChangeString(0, 0), "returns blank when both were 0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("yesterdayCountTitle with valid values", function() {
|
||||||
|
var title = reportWithData([6,8,5,2,1]).get('yesterdayCountTitle');
|
||||||
|
ok(title.indexOf('+60%') !== -1);
|
||||||
|
ok(title.match(/Was 5/));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("yesterdayCountTitle when two days ago was 0", function() {
|
||||||
|
var title = reportWithData([6,8,0,2,1]).get('yesterdayCountTitle');
|
||||||
|
equal(title.indexOf('%'), -1);
|
||||||
|
ok(title.match(/Was 0/));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("sevenDayCountTitle", function() {
|
||||||
|
var title = reportWithData([100,1,1,1,1,1,1,1,2,2,2,2,2,2,2,100,100]).get('sevenDayCountTitle');
|
||||||
|
ok(title.match(/-50%/));
|
||||||
|
ok(title.match(/Was 14/));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("thirtyDayCountTitle", function() {
|
||||||
|
var report = reportWithData([5,5,5,5]);
|
||||||
|
report.set('prev30Days', 10);
|
||||||
|
var title = report.get('thirtyDayCountTitle');
|
||||||
|
|
||||||
|
ok(title.indexOf('+50%') !== -1);
|
||||||
|
ok(title.match(/Was 10/));
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*global module:true test:true ok:true visit:true expect:true exists:true count:true present:true equal:true */
|
||||||
|
|
||||||
|
module("Discourse.UserAction");
|
||||||
|
|
||||||
|
test("collapsing likes", function () {
|
||||||
|
var actions = Discourse.UserAction.collapseStream([
|
||||||
|
Discourse.UserAction.create({
|
||||||
|
action_type: Discourse.UserAction.LIKE,
|
||||||
|
topic_id: 1,
|
||||||
|
user_id: 1,
|
||||||
|
post_number: 1
|
||||||
|
}), Discourse.UserAction.create({
|
||||||
|
action_type: Discourse.UserAction.EDIT,
|
||||||
|
topic_id: 2,
|
||||||
|
user_id: 1,
|
||||||
|
post_number: 1
|
||||||
|
}), Discourse.UserAction.create({
|
||||||
|
action_type: Discourse.UserAction.LIKE,
|
||||||
|
topic_id: 1,
|
||||||
|
user_id: 2,
|
||||||
|
post_number: 1
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
equal(actions.length, 2);
|
||||||
|
|
||||||
|
equal(actions[0].get('children.length'), 1);
|
||||||
|
equal(actions[0].get('children')[0].items.length, 2);
|
||||||
|
});
|
|
@ -52,6 +52,7 @@ sinon.config = {
|
||||||
|
|
||||||
// Trick JSHint into allow document.write
|
// Trick JSHint into allow document.write
|
||||||
var d = document;
|
var d = document;
|
||||||
|
d.write('<div id="qunit-scratch" style="display:none"></div>');
|
||||||
d.write('<div id="ember-testing-container"><div id="ember-testing"></div></div>');
|
d.write('<div id="ember-testing-container"><div id="ember-testing"></div></div>');
|
||||||
d.write('<style>#ember-testing-container { position: absolute; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; } #ember-testing { zoom: 50%; }</style>');
|
d.write('<style>#ember-testing-container { position: absolute; background: white; bottom: 0; right: 0; width: 640px; height: 384px; overflow: auto; z-index: 9999; border: 1px solid #ccc; } #ember-testing { zoom: 50%; }</style>');
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue