From 3826e670077c19031b005c0e2484098530e25aca Mon Sep 17 00:00:00 2001 From: Gilles Manzato Date: Fri, 1 Nov 2013 15:27:01 +0100 Subject: [PATCH 01/25] Flatter design on User Page - Cleanup of the design ( remove border-radius ... ) - Fixed some alignment and width in mobile and lower screen size --- .../common/components/navs.css.scss | 6 +- .../common/foundation/variables.scss | 3 +- app/assets/stylesheets/desktop/user.scss | 21 +++---- app/assets/stylesheets/mobile/user.scss | 59 +++++++++++++++---- 4 files changed, 60 insertions(+), 29 deletions(-) diff --git a/app/assets/stylesheets/common/components/navs.css.scss b/app/assets/stylesheets/common/components/navs.css.scss index 9206fece1ad07..8399c97e791da 100644 --- a/app/assets/stylesheets/common/components/navs.css.scss +++ b/app/assets/stylesheets/common/components/navs.css.scss @@ -56,7 +56,6 @@ padding: 0; overflow: hidden; background-color: $nav-stacked-background-color; - @include border-radius-all(4px); @include box-shadow(0 1px 0 $white); > li { border-bottom: 1px solid $nav-stacked-divider-color; @@ -66,16 +65,15 @@ > a { margin: 0; padding: 13px 13px 13px 30px; - font-weight: bold; font-size: 16px; line-height: 20px; cursor: pointer; + color: $nav-stacked-color; } } .active > a, .active .icon-chevron-right { - color: $nav-stacked-border-color-active; - text-shadow: 0 1px 0 rgba($white, 0.5); + color: $white; background-color: $nav-stacked-background-color-active; } .count { diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index bdb3972c4b2f2..045588dc4d23c 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -71,12 +71,13 @@ $nav-pills-background-color-active: #e45735 !default; // Stacked nav +$nav-stacked-color: #534d4b !default; $nav-stacked-border-color: #ccc !default; $nav-stacked-background-color: #fafafa !default; $nav-stacked-divider-color: #e6e6e6 !default; $nav-stacked-chevron-color: #ccc !default; $nav-stacked-border-color-active: #f15b22 !default; -$nav-stacked-background-color-active: #f9e7e0 !default; +$nav-stacked-background-color-active: #e45735 !default; // Button nav diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 505f543edc6dc..e7cdcd18380ab 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -40,7 +40,6 @@ color: $darkish_gray; } .warning { - @include border-radius-all(6px); background-color: lighten($red, 10%); padding: 5px 8px; color: $white; @@ -110,16 +109,16 @@ } .user-main { - width: 840px; + width: 850px; float: left; margin-bottom: 50px; @include medium-width { - width: 730px; + width: 735px; } @include small-width { - width: 650px; + width: 680px; } .user-content { @@ -127,21 +126,18 @@ background-color: white; border: 1px solid #ddd; margin-bottom: 10px; - @include border-radius-all(4px); } .about { background-color: #444; margin-bottom: 10px; overflow: hidden; - border: 1px solid #bbb; color: #fff; - @include border-radius-all(4px); .details { text-align: center; - padding: 10px; + padding: 12px; h1 { @@ -188,11 +184,12 @@ text-align: right; padding: 0 10px; width: 20%; - @include border-radius-all(4px); dd { color: white; margin: 0 0 7px 0; + overflow: hidden; + text-overflow: ellipsis; } dt { color: #aaa; @@ -203,9 +200,8 @@ .controls { background-color: #ddd; - margin-top: 10px; - padding: 5px; - height: 32px; + padding: 12px; + height: 30px; .right { float: right; @@ -273,7 +269,6 @@ background-color: white; border: 1px solid #ddd; margin-bottom: 10px; - @include border-radius-all(4px); } .type { color: lighten($black, 40%); diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 4ac9aeb6429ab..b74db3de66129 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -47,7 +47,6 @@ width: 220px; min-height: 200px; background-color: #f8f8f8; - border-radius: 5px; color: #444; word-wrap: break-word; } @@ -56,8 +55,12 @@ h3 { color: #666; + padding-left: 5px; + } + .nav-stacked { + border-left: 0; + border-right: 0; } - .summary { height: 50px; } @@ -112,11 +115,8 @@ background-color: #444; margin-bottom: 10px; overflow: hidden; - border: 1px solid #bbb; color: #fff; - @include border-radius-all(4px); - .details { text-align: center; padding: 10px; @@ -145,7 +145,6 @@ background-color: #222; text-align: left; padding: 0 10px; - @include border-radius-all(4px); dd { color: white; @@ -161,9 +160,49 @@ .controls { background-color: #ddd; margin-top: 0px; - padding: 5px; + padding: 10px 10px 0 10px; + + .btn { + margin-bottom: 10px; + float: none; + } + } + } + + .about.collapsed-info { + .controls { + margin-top: 0; + } - button { margin-bottom: 3px; } + .details { + .secondary { display: none; } + .bio { display: none; } + + .primary { + width: 100%; + text-align: left; + margin-top: 4px; + + .avatar { + float: left; + margin-right: 10px; + border: 2px solid white; + width: 45px; + height: 45px; + } + + h1 { + margin:0; + font-size: 20px; + line-height: 22px; + } + + h2 { + font-size: 17px; + line-height: 16px; + margin-top: 4px; + } + } } } @@ -188,9 +227,7 @@ .item { padding: 10px 8px; background-color: white; - border: 1px solid #b9b9b9; - margin-bottom: 10px; - @include border-radius-all(4px); + border-bottom: 1px solid #b9b9b9; } .type { color: lighten($black, 40%); From 2e7696630b0219b2d4c2ed6a2e7ed0a39cabb1dc Mon Sep 17 00:00:00 2001 From: Scott Albertson Date: Fri, 1 Nov 2013 11:12:25 -0700 Subject: [PATCH 02/25] Make #update specs consistent * Use expect syntax * Avoid lets * Stub Guardian method used in the controller --- spec/controllers/users_controller_spec.rb | 52 ++++++++++++----------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 609242792c53c..9ed7902d578c9 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -824,40 +824,43 @@ def post_user end - describe '.update' do - - context 'not logged in' do - it 'raises an error when not logged in' do + describe '#update' do + context 'with guest' do + it 'raises an error' do expect do - xhr :put, :update, username: 'somename' + xhr :put, :update, username: 'guest' end.to raise_error(Discourse::NotLoggedIn) end end - context 'logged in' do - let!(:user) { log_in } + context 'with authenticated user' do + context 'with permission to update' do + it 'allows the update' do + user = Fabricate(:user, name: 'Billy Bob') + log_in_user(user) + guardian = Guardian.new(user) + guardian.stubs(:ensure_can_edit!) + Guardian.stubs(new: guardian).with(user) - context 'without a token' do - it 'should ensure you can update the user' do - Guardian.any_instance.expects(:can_edit?).with(user).returns(false) - put :update, username: user.username - response.should be_forbidden - end + put :update, username: user.username, name: 'Jim Tom' - context 'as a user who can edit the user' do + expect(response).to be_success + expect(user.reload.name).to eq 'Jim Tom' + end + end - before do - put :update, username: user.username, bio_raw: 'brand new bio' - user.reload - end + context 'without permission to update' do + it 'does not allow the update' do + user = Fabricate(:user, name: 'Billy Bob') + log_in_user(user) + guardian = Guardian.new(user) + guardian.stubs(:ensure_can_edit!).raises(Discourse::InvalidAccess.new) + Guardian.stubs(new: guardian).with(user) - it 'updates the user' do - user.bio_raw.should == 'brand new bio' - end + put :update, username: user.username, name: 'Jim Tom' - it 'returns json success' do - response.should be_success - end + expect(response).to be_forbidden + expect(user.reload.name).not_to eq 'Jim Tom' end end end @@ -1102,5 +1105,4 @@ def post_user end end - end From 58f96bdfb5a94b9df2b5a915f044d06a08bbc465 Mon Sep 17 00:00:00 2001 From: Scott Albertson Date: Fri, 1 Nov 2013 11:22:52 -0700 Subject: [PATCH 03/25] Remove duplication in test setup --- spec/controllers/users_controller_spec.rb | 31 +++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 9ed7902d578c9..c378af6d9565e 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -836,11 +836,10 @@ def post_user context 'with authenticated user' do context 'with permission to update' do it 'allows the update' do - user = Fabricate(:user, name: 'Billy Bob') - log_in_user(user) - guardian = Guardian.new(user) - guardian.stubs(:ensure_can_edit!) - Guardian.stubs(new: guardian).with(user) + user = create_authenticated_user('Billy Bob') + stub_guardian(user) do |guardian| + guardian.stubs(:ensure_can_edit!).with(user) + end put :update, username: user.username, name: 'Jim Tom' @@ -851,11 +850,11 @@ def post_user context 'without permission to update' do it 'does not allow the update' do - user = Fabricate(:user, name: 'Billy Bob') - log_in_user(user) - guardian = Guardian.new(user) - guardian.stubs(:ensure_can_edit!).raises(Discourse::InvalidAccess.new) - Guardian.stubs(new: guardian).with(user) + user = create_authenticated_user('Billy Bob') + stub_guardian(user) do |guardian| + guardian.stubs(:ensure_can_edit!). + with(user).raises(Discourse::InvalidAccess.new) + end put :update, username: user.username, name: 'Jim Tom' @@ -1105,4 +1104,16 @@ def post_user end end + + private + + def create_authenticated_user(name) + log_in_user(Fabricate(:user, name: name)) + end + + def stub_guardian(user) + guardian = Guardian.new(user) + yield(guardian) + Guardian.stubs(new: guardian).with(user) + end end From 3cc17ad4cdc4a08853b9dda6b90c1e4d0aab3afd Mon Sep 17 00:00:00 2001 From: Scott Albertson Date: Fri, 1 Nov 2013 11:42:16 -0700 Subject: [PATCH 04/25] Add test coverage for #update --- spec/controllers/users_controller_spec.rb | 57 ++++++++++++++--------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index c378af6d9565e..f8539afd7c145 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -836,25 +836,52 @@ def post_user context 'with authenticated user' do context 'with permission to update' do it 'allows the update' do - user = create_authenticated_user('Billy Bob') - stub_guardian(user) do |guardian| - guardian.stubs(:ensure_can_edit!).with(user) - end + user = Fabricate(:user, name: 'Billy Bob') + log_in_user(user) put :update, username: user.username, name: 'Jim Tom' expect(response).to be_success expect(user.reload.name).to eq 'Jim Tom' end + + it 'returns user JSON' do + user = log_in + + put :update, username: user.username + + json = JSON.parse(response.body) + expect(json['user']['id']).to eq user.id + end + + context 'when website includes http' do + it 'does not add http before updating' do + user = log_in + + put :update, username: user.username, website: 'http://example.com' + + expect(user.reload.website).to eq 'http://example.com' + end + end + + context 'when website does not include http' do + it 'adds http before updating' do + user = log_in + + put :update, username: user.username, website: 'example.com' + + expect(user.reload.website).to eq 'http://example.com' + end + end end context 'without permission to update' do it 'does not allow the update' do - user = create_authenticated_user('Billy Bob') - stub_guardian(user) do |guardian| - guardian.stubs(:ensure_can_edit!). - with(user).raises(Discourse::InvalidAccess.new) - end + user = Fabricate(:user, name: 'Billy Bob') + log_in_user(user) + guardian = Guardian.new(user) + guardian.stubs(:ensure_can_edit!).with(user).raises(Discourse::InvalidAccess.new) + Guardian.stubs(new: guardian).with(user) put :update, username: user.username, name: 'Jim Tom' @@ -1104,16 +1131,4 @@ def post_user end end - - private - - def create_authenticated_user(name) - log_in_user(Fabricate(:user, name: name)) - end - - def stub_guardian(user) - guardian = Guardian.new(user) - yield(guardian) - Guardian.stubs(new: guardian).with(user) - end end From c0cffca1e65d7db4d5a6fe0edbb30438dcc03727 Mon Sep 17 00:00:00 2001 From: Scott Albertson Date: Fri, 1 Nov 2013 13:27:48 -0700 Subject: [PATCH 05/25] Test title updating --- spec/controllers/users_controller_spec.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index f8539afd7c145..d5d2a24f0b6c8 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -875,7 +875,7 @@ def post_user end end - context 'without permission to update' do + context 'without permission to update any attributes' do it 'does not allow the update' do user = Fabricate(:user, name: 'Billy Bob') log_in_user(user) @@ -889,6 +889,20 @@ def post_user expect(user.reload.name).not_to eq 'Jim Tom' end end + + context 'without permission to update title' do + it 'does not allow the user to update their title' do + user = Fabricate(:user, title: 'Emperor') + log_in_user(user) + guardian = Guardian.new(user) + guardian.stubs(can_grant_title?: false).with(user) + Guardian.stubs(new: guardian).with(user) + + put :update, username: user.username, title: 'Minion' + + expect(user.reload.title).not_to eq 'Minion' + end + end end end From b56b11d96aafa5cd9e2f004ae631348fba4574ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 1 Nov 2013 23:57:50 +0100 Subject: [PATCH 06/25] add qunit to autospec --- .../users/omniauth_callbacks_controller.rb | 6 +- lib/auth/default_current_user_provider.rb | 7 +- lib/autospec/base_runner.rb | 24 +- lib/autospec/formatter.rb | 53 +-- lib/autospec/manager.rb | 241 ++++++++++++++ lib/autospec/qunit_runner.rb | 150 +++++++++ lib/autospec/reload_css.rb | 22 +- lib/autospec/rspec_runner.rb | 43 +++ lib/autospec/run-qunit.js | 175 ++++++++++ lib/autospec/runner.rb | 308 ------------------ lib/autospec/simple_runner.rb | 34 +- lib/autospec/spork_runner.rb | 40 ++- lib/demon/base.rb | 142 ++++++++ lib/demon/rails_autospec.rb | 25 ++ lib/demon/sidekiq.rb | 168 +--------- lib/tasks/autospec.rake | 21 +- spec/serializers/user_serializer_spec.rb | 1 - vendor/assets/javascripts/run-qunit.js | 16 +- 18 files changed, 913 insertions(+), 563 deletions(-) create mode 100644 lib/autospec/manager.rb create mode 100644 lib/autospec/qunit_runner.rb create mode 100644 lib/autospec/rspec_runner.rb create mode 100644 lib/autospec/run-qunit.js delete mode 100644 lib/autospec/runner.rb create mode 100644 lib/demon/base.rb create mode 100644 lib/demon/rails_autospec.rb diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index a0d510aed0bcd..b629228188b63 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -62,16 +62,12 @@ def self.find_authenticator(name) BUILTIN_AUTH.each do |authenticator| if authenticator.name == name raise Discourse::InvalidAccess.new("provider is not enabled") unless SiteSetting.send("enable_#{name}_logins?") - return authenticator end end Discourse.auth_providers.each do |provider| - if provider.name == name - - return provider.authenticator - end + return provider.authenticator if provider.name == name end raise Discourse::InvalidAccess.new("provider is not found") diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 1eb90b22b7cbe..98430e3207247 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -2,10 +2,9 @@ class Auth::DefaultCurrentUserProvider - CURRENT_USER_KEY = "_DISCOURSE_CURRENT_USER" - API_KEY = "_DISCOURSE_API" - - TOKEN_COOKIE = "_t" + CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER" + API_KEY ||= "_DISCOURSE_API" + TOKEN_COOKIE ||= "_t" # do all current user initialization here def initialize(env) diff --git a/lib/autospec/base_runner.rb b/lib/autospec/base_runner.rb index b3e884c0c4da7..30bd513a71465 100644 --- a/lib/autospec/base_runner.rb +++ b/lib/autospec/base_runner.rb @@ -1,22 +1,36 @@ module Autospec + class BaseRunner - def run(args, specs) + + # used when starting the runner - preloading happens here + def start(opts = {}) end - def abort + # indicates whether tests are running + def running? + true + end + + # launch a batch of specs/tests + def run(specs) end + # used when we need to reload the whole application def reload end - def running? - true + # used to abort the current run + def abort end - def start + def failed_specs + [] end + # used to stop the runner def stop end + end + end diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb index 32bdd810595bb..d3ec0d892f6d9 100644 --- a/lib/autospec/formatter.rb +++ b/lib/autospec/formatter.rb @@ -1,39 +1,46 @@ -require "rspec/core/formatters/base_formatter" +require "rspec/core/formatters/base_text_formatter" module Autospec; end -class Autospec::Formatter < RSpec::Core::Formatters::BaseFormatter - - def dump_summary(duration, total, failures, pending) - # failed_specs = examples.delete_if{|e| e.execution_result[:status] != "failed"}.map{|s| s.metadata[:location]} - - # # if this fails don't kill everything - # begin - # FileUtils.mkdir_p('tmp') - # File.open("./tmp/rspec_result","w") do |f| - # f.puts failed_specs.join("\n") - # end - # rescue - # # nothing really we can do, at least don't kill the test runner - # end + +class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter + + RSPEC_RESULT = "./tmp/rspec_result" + + def initialize(output) super + FileUtils.mkdir_p("tmp") unless Dir.exists?("tmp") end - def start(count) - FileUtils.mkdir_p('tmp') - @fail_file = File.open("./tmp/rspec_result","w") - super(count) + def start(example_count) + super + File.delete(RSPEC_RESULT) if File.exists?(RSPEC_RESULT) + @fail_file = File.open(RSPEC_RESULT,"w") end - def close - @fail_file.close + def example_passed(example) + super + output.print success_color(".") + end + + def example_pending(example) super + output.print pending_color("*") end def example_failed(example) - @fail_file.puts example.metadata[:location] + super + output.print failure_color("F") + @fail_file.puts(example.metadata[:location] + " ") @fail_file.flush - super(example) end + def start_dump + super + output.puts + end + + def close + @fail_file.close + end end diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb new file mode 100644 index 0000000000000..2d9760833d743 --- /dev/null +++ b/lib/autospec/manager.rb @@ -0,0 +1,241 @@ +require "listen" +require "thread" +require "fileutils" +require "autospec/reload_css" +require "autospec/base_runner" + +module Autospec; end + +class Autospec::Manager + + def self.run(opts={}) + self.new.run(opts) + end + + def initialize + @queue = [] + @mutex = Mutex.new + @signal = ConditionVariable.new + end + + def run(opts = {}) + @runners = [ruby_runner, javascript_runner] + + Signal.trap("HUP") { stop_runners; exit } + Signal.trap("INT") { stop_runners; exit } + + ensure_all_specs_will_run + start_runners + start_service_queue + listen_for_changes + + puts "Press [ENTER] to stop the current run" + while @runners.any?(&:running?) + STDIN.gets + process_queue + end + + rescue => e + fail(e, "failed in run") + ensure + stop_runners + end + + private + + def ruby_runner + if ENV["SPORK"] + require "autospec/spork_runner" + Autospec::SporkRunner.new + else + require "autospec/simple_runner" + Autospec::SimpleRunner.new + end + end + + def javascript_runner + require "autospec/qunit_runner" + Autospec::QunitRunner.new + end + + def ensure_all_specs_will_run + @runners.each do |runner| + @queue << ['spec', 'spec', runner] unless @queue.any? { |f, s, r| s == "spec" && r == runner } + end + end + + [:start, :stop, :abort].each do |verb| + define_method("#{verb}_runners") do + @runners.each(&verb) + end + end + + def start_service_queue + Thread.new do + while true + thread_loop + end + end + end + + # the main loop, will run the specs in the queue till one fails or the queue is empty + def thread_loop + @mutex.synchronize do + current = @queue.first + last_failed = false + last_failed = process_spec(current) if current + # stop & wait for the queue to have at least one item or when there's been a failure + @signal.wait(@mutex) if @queue.length == 0 || last_failed + end + rescue => e + fail(e, "failed in main loop") + end + + # will actually run the spec and check whether the spec has failed or not + def process_spec(current) + has_failed = false + # retrieve the instance of the runner + runner = current[2] + # actually run the spec (blocking call) + result = runner.run(current[1]).to_i + + if result == 0 + # remove the spec from the queue + @queue.shift + else + has_failed = true + if result > 0 + focus_on_failed_tests(current) + ensure_all_specs_will_run + end + end + + has_failed + end + + def focus_on_failed_tests(current) + runner = current[2] + # we only want 1 focus in the queue + @queue.shift if current[0] == "focus" + # focus on the first 10 failed specs + failed_specs = runner.failed_specs[0..10] + # focus on the failed specs + @queue.unshift ["focus", failed_specs.join(" "), runner] if failed_specs.length > 0 + end + + def listen_for_changes(opts = {}) + options = { + ignore: /^public|^lib\/autospec/, + relative_paths: true, + } + + if opts[:force_polling] + options[:force_polling] = true + options[:latency] = opts[:latency] || 3 + end + + Thread.start do + Listen.to('.', options) do |modified, added, removed| + process_change([modified, added].flatten.compact) + end + end + end + + def process_change(files) + return if files.length == 0 + specs = [] + hit = false + + files.each do |file| + @runners.each do |runner| + # reloaders + runner.reloaders.each do |k| + if k.match(file) + runner.reload + return + end + end + # watchers + runner.watchers.each do |k,v| + if m = k.match(file) + hit = true + spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file + specs << [file, spec, runner] if File.exists?(spec) || Dir.exists?(spec) + end + end + end + # special watcher for styles/templates + Autospec::ReloadCss::WATCHERS.each do |k,v| + matches = [] + matches << file if k.match(file) + Autospec::ReloadCss.run_on_change(matches) if matches.present? + end + end + + queue_specs(specs) if hit + + rescue => e + fail(e, "failed in watcher") + end + + def queue_specs(specs) + if specs.length == 0 + locked = @mutex.try_lock + if locked + @signal.signal + @mutex.unlock + end + return + else + abort_runners + end + + @mutex.synchronize do + specs.each do |file, spec, runner| + # make sure there's no other instance of this spec in the queue + @queue.delete_if { |f, s, r| s.strip == spec.strip && r == runner } + # deal with focused specs + if @queue.first && @queue.first[0] == "focus" + focus = @queue.shift + @queue.unshift([file, spec, runner]) + if focus[1].include?(spec) || file != spec + @queue.unshift(focus) + end + else + @queue.unshift([file, spec, runner]) + end + end + @signal.signal + end + end + + def process_queue + if @queue.length == 0 + ensure_all_specs_will_run + @signal.signal + else + current = @queue.first + runner = current[2] + specs = runner.failed_specs + puts + puts + if specs.length == 0 + puts "No specs have failed yet!" + puts + else + puts "The following specs have failed:" + specs.each { |s| puts s } + puts + specs = specs.map { |s| [s, s, runner] } + queue_specs(specs) + end + end + end + + def fail(exception, message = nil) + puts message if message + puts exception.message + puts exception.backtrace.join("\n") + end + +end diff --git a/lib/autospec/qunit_runner.rb b/lib/autospec/qunit_runner.rb new file mode 100644 index 0000000000000..3dae8a2f24932 --- /dev/null +++ b/lib/autospec/qunit_runner.rb @@ -0,0 +1,150 @@ +require "demon/rails_autospec" + +module Autospec + + class QunitRunner < BaseRunner + + WATCHERS = {} + def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end + def watchers; WATCHERS; end + + # Discourse specific + watch(%r{^app/assets/javascripts/discourse/(.+)\.js$}) { |m| "test/javascripts/#{m[1]}_test.js" } + watch(%r{^app/assets/javascripts/admin/(.+)\.js$}) { |m| "test/javascripts/admin/#{m[1]}_test.js" } + watch(%r{^test/javascripts/.+\.js$}) + + RELOADERS = Set.new + def self.reload(pattern); RELOADERS << pattern; end + def reloaders; RELOADERS; end + + # Discourse specific + reload(%r{^test/javascripts/fixtures/.+_fixtures\.js$}) + reload(%r{^test/javascripts/(helpers|mixins)/.+\.js$}) + reload("test/javascripts/test_helper.js") + + require "socket" + + class PhantomJsNotInstalled < Exception; end + + def initialize + ensure_phantomjs_is_installed + end + + def start + # ensure we can launch the rails server + unless port_available?(port) + puts "Port #{port} is not available" + puts "Either kill the process using that port or use the `TEST_SERVER_PORT` environment variable" + return + end + + # start rails + start_rails_server + @running = true + end + + def running? + @running + end + + def run(specs) + puts "Running Qunit: #{specs}" + + abort + + qunit_url = "http://localhost:#{port}/qunit" + + if specs != "spec" && specs.split.length == 1 + module_name = try_to_find_module_name(specs.strip) + qunit_url << "?module=#{module_name}" if module_name + end + + cmd = "phantomjs #{Rails.root}/lib/autospec/run-qunit.js \"#{qunit_url}\"" + + @pid = Process.spawn(cmd) + _, status = Process.wait2(@pid) + + status.exitstatus + end + + def reload + stop_rails_server + sleep 1 + start_rails_server + end + + def abort + if @pid + children_processes(@pid).each { |pid| kill_process(pid) } + kill_process(@pid) + @pid = nil + end + end + + def failed_specs + specs = [] + path = './tmp/qunit_result' + specs = File.readlines(path) if File.exist?(path) + specs + end + + def stop + # kill phantomjs first + abort + stop_rails_server + @running = false + end + + private + + def ensure_phantomjs_is_installed + raise PhantomJsNotInstalled.new unless system("command -v phantomjs >/dev/null;") + end + + def port_available?(port) + TCPServer.open(port).close + true + rescue Errno::EADDRINUSE + false + end + + def port + @port ||= ENV["TEST_SERVER_PORT"] || 60099 + end + + def start_rails_server + Demon::RailsAutospec.start(1) + end + + def stop_rails_server + Demon::RailsAutospec.stop + end + + def children_processes(base = Process.pid) + process_tree = Hash.new { |hash, key| hash[key] = [key] } + Hash[*`ps -eo pid,ppid`.scan(/\d+/).map(&:to_i)].each do |pid, ppid| + process_tree[ppid] << process_tree[pid] + end + process_tree[base].flatten - [base] + end + + def kill_process(pid) + return unless pid + Process.kill("INT", pid) rescue nil + while (Process.getpgid(pid) rescue nil) + sleep 0.001 + end + end + + def try_to_find_module_name(file) + return unless File.exists?(file) + File.open(file, "r").each_line do |line| + if m = /module\(['"]([^'"]+)/i.match(line) + return m[1] + end + end + end + + end + +end diff --git a/lib/autospec/reload_css.rb b/lib/autospec/reload_css.rb index ab4d9f74dc3fe..5c69bdf94d680 100644 --- a/lib/autospec/reload_css.rb +++ b/lib/autospec/reload_css.rb @@ -1,23 +1,23 @@ module Autospec; end + class Autospec::ReloadCss - MATCHERS = {} + WATCHERS = {} def self.watch(pattern, &blk) - MATCHERS[pattern] = blk + WATCHERS[pattern] = blk end - watch(/tmp\/refresh_browser/) + # css, scss, sass or handlebars watch(/\.css$/) - watch(/\.css\.erb$/) - watch(/\.sass$/) - watch(/\.scss$/) - watch(/\.sass\.erb$/) + watch(/\.ca?ss\.erb$/) + watch(/\.s[ac]ss$/) watch(/\.handlebars$/) def self.message_bus MessageBus::Instance.new.tap do |bus| bus.site_id_lookup do - # this is going to be dev the majority of the time, if you have multisite configured in dev stuff may be different + # this is going to be dev the majority of the time + # if you have multisite configured in dev stuff may be different "default" end end @@ -26,13 +26,13 @@ def self.message_bus def self.run_on_change(paths) paths.map! do |p| hash = nil - fullpath = Rails.root.to_s + "/" + p - hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists? fullpath + fullpath = "#{Rails.root}/#{p}" + hash = Digest::MD5.hexdigest(File.read(fullpath)) if File.exists?(fullpath) p = p.sub /\.sass\.erb/, "" p = p.sub /\.sass/, "" p = p.sub /\.scss/, "" p = p.sub /^app\/assets\/stylesheets/, "assets" - {name: p, hash: hash} + { name: p, hash: hash } end message_bus.publish "/file-change", paths end diff --git a/lib/autospec/rspec_runner.rb b/lib/autospec/rspec_runner.rb new file mode 100644 index 0000000000000..fe5f23703e87d --- /dev/null +++ b/lib/autospec/rspec_runner.rb @@ -0,0 +1,43 @@ +module Autospec + + class RspecRunner < BaseRunner + + WATCHERS = {} + def self.watch(pattern, &blk); WATCHERS[pattern] = blk; end + def watchers; WATCHERS; end + + # Discourse specific + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } + + # Rails example + watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^spec/support/.+\.rb$}) { "spec" } + watch("app/controllers/application_controller.rb") { "spec/controllers" } + + # Capybara request specs + watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } + + # Fabrication + watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } + + RELOADERS = Set.new + def self.reload(pattern); RELOADERS << pattern; end + def reloaders; RELOADERS; end + + # We need to reload the whole app when changing any of these files + reload('spec/spec_helper.rb') + reload('config/(.*).rb') + reload('app/helpers/(.*).rb') + + def failed_specs + specs = [] + path = './tmp/rspec_result' + specs = File.readlines(path) if File.exist?(path) + specs + end + + end + +end diff --git a/lib/autospec/run-qunit.js b/lib/autospec/run-qunit.js new file mode 100644 index 0000000000000..15804f269de32 --- /dev/null +++ b/lib/autospec/run-qunit.js @@ -0,0 +1,175 @@ +// THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC + +if (phantom.args.length != 1) { + console.log("Usage: " + phantom.scriptName + " "); + phantom.exit(1); +} + +var system = require('system'), + fs = require('fs'), + page = require('webpage').create(), + QUNIT_RESULT = "./tmp/qunit_result"; + +if (fs.exists(QUNIT_RESULT) && fs.isFile(QUNIT_RESULT)) { fs.remove(QUNIT_RESULT); } + +page.onConsoleMessage = function (message) { + // filter out Ember's debug messages + if (message.slice(0, 8) === "WARNING:") { return; } + if (message.slice(0, 6) === "DEBUG:") { return; } + + console.log(message); +}; + +page.onCallback = function (message) { + // write to the result file + if (message.slice(0, 5) === "FILE:") { fs.write(QUNIT_RESULT, message.slice(6), "a"); } + // forward the message to the standard output + if (message.slice(0, 6) === "PRINT:") { system.stdout.write(message.slice(7)); } +}; + +page.start = new Date(); + +// -----------------------------------WARNING -------------------------------------- +// calling "console.log" BELOW this line will go through the "page.onConsoleMessage" +// -----------------------------------WARNING -------------------------------------- +page.open(phantom.args[0], function (status) { + if (status !== "success") { + console.log("\nNO NETWORK :(\n"); + phantom.exit(1); + } else { + console.log("QUnit loaded in " + (new Date() - page.start) + " ms"); + + page.evaluate(colorizer); + page.evaluate(logQUnit); + + // wait up to 60 seconds for QUnit to finish + var timeout = 60 * 1000, + start = Date.now(); + + var interval = setInterval(function() { + if (Date.now() - start > timeout) { + console.error("\nTIME OUT :(\n"); + phantom.exit(1); + } else { + var qunitResult = page.evaluate(function() { return window.qunitResult; }); + if (qunitResult) { + clearInterval(interval); + if (qunitResult.failed > 0) { + phantom.exit(1); + } else { + phantom.exit(0); + } + } + } + }, 250); + } +}); + +// https://github.com/jquery/qunit/pull/470 +function colorizer() { + window.ANSI = { + colorMap: { + "red": "\u001b[31m", + "green": "\u001b[32m", + "blue": "\u001b[34m", + "end": "\u001b[0m" + }, + highlightMap: { + "red": "\u001b[41m\u001b[37m", // change 37 to 30 for black text + "green": "\u001b[42m\u001b[30m", + "blue": "\u001b[44m\u001b[37m", + "end": "\u001b[0m" + }, + + highlight: function (text, color) { + var colorCode = this.highlightMap[color], + colorEnd = this.highlightMap.end; + + return colorCode + text + colorEnd; + }, + + colorize: function (text, color) { + var colorCode = this.colorMap[color], + colorEnd = this.colorMap.end; + + return colorCode + text + colorEnd; + } + }; +}; + + +function logQUnit() { + // keep track of error messages + var errors = {}; + + QUnit.begin(function () { + console.log("BEGIN"); + }); + + QUnit.log(function (context) { + if (!context.result) { + var module = context.module, + test = context.name; + + var assertion = { + message: context.message, + expected: context.expected, + actual: context.actual + }; + + if (!errors[module]) { errors[module] = {}; } + if (!errors[module][test]) { errors[module][test] = []; } + errors[module][test].push(assertion); + + var fileName = context.source + .replace(/[^\S\n]+at[^\S\n]+/g, "") + .split("\n")[1] + .replace(/\?.+$/, "") + .replace(/^.+\/assets\//, "test/javascripts/"); + window.callPhantom("FILE: " + fileName + " "); + } + }); + + QUnit.testDone(function (context) { + if (context.failed > 0) { + window.callPhantom("PRINT: " + ANSI.colorize("F", "red")); + } else { + window.callPhantom("PRINT: " + ANSI.colorize(".", "green")); + } + }); + + QUnit.done(function (context) { + console.log("\n"); + + // display failures + if (Object.keys(errors).length > 0) { + console.log("Failures:\n"); + for (m in errors) { + var module = errors[m]; + console.log("Module Failed: " + ANSI.highlight(m, "red")); + for (t in module) { + var test = module[t]; + console.log(" Test Failed: " + t); + for (var a = 0; a < test.length; a++) { + var assertion = test[a]; + console.log(" Assertion Failed: " + (assertion.message || "")); + if (assertion.expected) { + console.log(" Expected: " + assertion.expected); + console.log(" Actual: " + assertion.actual); + } + } + } + } + } + + // display summary + console.log("\n"); + console.log("Finished in " + (context.runtime / 1000) + " seconds"); + var color = context.failed > 0 ? "red" : "green"; + console.log(ANSI.colorize(context.total + " examples, " + context.failed + " failures", color)); + + // we're done + window.qunitResult = context; + }); + +}; diff --git a/lib/autospec/runner.rb b/lib/autospec/runner.rb deleted file mode 100644 index 7ba9cbb52957b..0000000000000 --- a/lib/autospec/runner.rb +++ /dev/null @@ -1,308 +0,0 @@ -require "drb/drb" -require "thread" -require "fileutils" -require "autospec/reload_css" -require "autospec/base_runner" -require "autospec/simple_runner" -require "autospec/spork_runner" - -module Autospec; end - -class Autospec::Runner - MATCHERS = {} - def self.watch(pattern, &blk) - MATCHERS[pattern] = blk - end - - watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } - - # Rails example - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } - watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb" } - watch(%r{^spec/support/(.+)\.rb$}) { "spec" } - watch("app/controllers/application_controller.rb") { "spec/controllers" } - - # Capybara request specs - watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } - - # Fabrication - watch(%r{^spec/fabricators/(.+)_fabricator\.rb$}) { "spec" } - - RELOAD_MATCHERS = Set.new - def self.watch_reload(pattern) - RELOAD_MATCHERS << pattern - end - - watch_reload('spec/spec_helper.rb') - watch_reload('config/(.*).rb') - watch_reload(%r{app/helpers/(.*).rb}) - - def self.run(opts={}) - self.new.run(opts) - end - - def initialize - @queue = [] - @mutex = Mutex.new - @signal = ConditionVariable.new - start_service_queue - end - - def run(opts = {}) - - puts "Forced polling (slower) - inotify does not work on network filesystems, use local filesystem to avoid" if opts[:force_polling] - - if ENV["SPORK"] == "0" - puts "Using Simple Runner" - @runner = Autospec::SimpleRunner.new - else - puts "Using Spork Runner" - @runner = Autospec::SporkRunner.new - end - @runner.start - - Signal.trap("HUP") {@runner.stop; exit } - Signal.trap("SIGINT") {@runner.stop; exit } - - options = {filter: /^app|^spec|^lib/, relative_paths: true} - - if opts[:force_polling] - options[:force_polling] = true - options[:latency] = opts[:latency] || 3 - end - - Thread.start do - Listen.to('.', options ) do |modified, added, removed| - process_change([modified, added].flatten.compact) - end - end - - @mutex.synchronize do - @queue << ['spec', 'spec'] - @signal.signal - end - - while @runner.running? - process_queue - end - - rescue => e - puts e - puts e.backtrace - @runner.stop - end - - def process_queue - STDIN.gets - - if @queue.length == 0 - @queue << ['spec', 'spec'] - @signal.signal - else - specs = failed_specs(:delete => false) - puts - puts - if specs.length == 0 - puts "No specs have failed yet!" - puts - else - puts "The following specs have failed: " - specs.each do |s| - puts s - end - puts - queue_specs(specs.zip specs) - end - end - end - - def wait_for(timeout_milliseconds) - timeout = (timeout_milliseconds + 0.0) / 1000 - finish = Time.now + timeout - t = Thread.new do - while Time.now < finish && !yield - sleep(0.001) - end - end - t.join rescue nil - end - - def force_polling? - works = false - - begin - require 'rb-inotify' - require 'fileutils' - n = INotify::Notifier.new - FileUtils.touch('./tmp/test_polling') - - n.watch("./tmp", :modify, :attrib){ works = true } - quit = false - Thread.new do - while !works && !quit - if IO.select([n.to_io], [], [], 0.1) - n.process - end - end - end - sleep 0.01 - - FileUtils.touch('./tmp/test_polling') - wait_for(100) { works } - File.unlink('./tmp/test_polling') - n.stop - quit = true - rescue LoadError - #assume it works (mac) - works = true - end - - !works - end - - - def process_change(files) - return unless files.length > 0 - - specs = [] - hit = false - files.each do |file| - RELOAD_MATCHERS.each do |k| - if k.match(file) - @runner.reload - return - end - end - MATCHERS.each do |k,v| - if m = k.match(file) - hit = true - spec = v ? ( v.arity == 1 ? v.call(m) : v.call ) : file - if File.exists?(spec) || Dir.exists?(spec) - specs << [file, spec] - end - end - end - Autospec::ReloadCss::MATCHERS.each do |k,v| - matches = [] - if k.match(file) - matches << file - end - Autospec::ReloadCss.run_on_change(matches) if matches.present? - end - end - queue_specs(specs) if hit - rescue => e - p "failed in watcher" - p e - p e.backtrace - end - - def queue_specs(specs) - if specs.length == 0 - locked = @mutex.try_lock - if locked - @signal.signal - @mutex.unlock - end - return - else - @runner.abort - end - - @mutex.synchronize do - specs.each do |c,spec| - @queue.delete([c,spec]) - if @queue.last && @queue.last[0] == "focus" - focus = @queue.pop - @queue << [c,spec] - if focus[1].include?(spec) || c != spec - @queue << focus - end - else - @queue << [c,spec] - end - end - @signal.signal - end - end - - def thread_loop - @mutex.synchronize do - last_failed = false - current = @queue.last - if current - last_failed = process_spec(current[1]) - end - wait = @queue.length == 0 || last_failed - @signal.wait(@mutex) if wait - end - rescue => e - p "DISASTA PASTA" - puts e - puts e.backtrace - end - - def process_spec(spec) - last_failed = false - result = run_spec(spec) - if result == 0 - @queue.pop - else - last_failed = true - if result.to_i > 0 - focus_on_failed_tests - ensure_all_specs_will_run - end - end - - last_failed - end - - def start_service_queue - @worker ||= Thread.new do - while true - thread_loop - end - end - end - - def focus_on_failed_tests - current = @queue.last - specs = failed_specs[0..10] - if current[0] == "focus" - @queue.pop - end - @queue << ["focus", specs.join(" ")] - end - - def ensure_all_specs_will_run - unless @queue.any?{|s,t| t == 'spec'} - @queue.unshift(['spec','spec']) - end - end - - def failed_specs(opts={:delete => true}) - specs = [] - path = './tmp/rspec_result' - if File.exist?(path) - specs = File.open(path) { |file| file.read.split("\n") } - File.delete(path) if opts[:delete] - end - - specs - end - - def run_spec(specs) - File.delete("tmp/rspec_result") if File.exists?("tmp/rspec_result") - args = ["-f", "progress", specs.split(" "), - "-r", "#{File.dirname(__FILE__)}/formatter.rb", - "-f", "Autospec::Formatter"].flatten - - @runner.run(args, specs) - - end - - -end diff --git a/lib/autospec/simple_runner.rb b/lib/autospec/simple_runner.rb index d43cb32abb858..e94d3d93413ce 100644 --- a/lib/autospec/simple_runner.rb +++ b/lib/autospec/simple_runner.rb @@ -1,26 +1,36 @@ +require "autospec/rspec_runner" + module Autospec - class SimpleRunner < BaseRunner + + class SimpleRunner < RspecRunner + + def run(specs) + puts "Running Rspec: " << specs + # kill previous rspec instance + abort + # we use our custom rspec formatter + args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb", + "-f", "Autospec::Formatter", specs.split].flatten.join(" ") + # launch rspec + @pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec #{args}") + _, status = Process.wait2(@pid) + status.exitstatus + end def abort if @pid - Process.kill("SIGINT", @pid) rescue nil - while(Process.getpgid(@pid) rescue nil) + Process.kill("INT", @pid) rescue nil + while (Process.getpgid(@pid) rescue nil) sleep 0.001 end @pid = nil end end - def run(args, spec) - self.abort - puts "Running: " << spec - @pid = Process.spawn({"RAILS_ENV" => "test"}, "bundle exec rspec " << args.join(" ")) - pid, status = Process.wait2(@pid) - status - end - def stop - self.abort + abort end + end + end diff --git a/lib/autospec/spork_runner.rb b/lib/autospec/spork_runner.rb index 95b3525418f34..fbd05d93ef5c4 100644 --- a/lib/autospec/spork_runner.rb +++ b/lib/autospec/spork_runner.rb @@ -1,5 +1,9 @@ +require "drb/drb" +require "autospec/rspec_runner" + module Autospec - class SporkRunner < BaseRunner + + class SporkRunner < RspecRunner def start if already_running?(pid_file) @@ -13,33 +17,38 @@ def start end def running? + # launch a thread that will wait for spork to die @monitor_thread ||= Thread.new do Process.wait(@spork_pid) @spork_running = false end + @spork_running end - def stop - stop_spork + def run(specs) + args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb", + "-f", "Autospec::Formatter", specs.split].flatten + spork_service.run(args,$stderr,$stdout) end - def run(args,specs) - spork_service.run(args,$stderr,$stdout) + def reload + stop_spork + sleep 1 + start_spork end def abort spork_service.abort end - def reload + def stop stop_spork - sleep 1 - start_spork end private + def spork_pid_file Rails.root + "tmp/pids/spork.pid" end @@ -55,7 +64,7 @@ def already_running?(pid_file) end end - def write_pid_file(file,pid) + def write_pid_file(file, pid) FileUtils.mkdir_p(Rails.root + "tmp/pids") File.open(file,'w') do |f| f.write(pid) @@ -67,25 +76,18 @@ def spork_running? end def spork_service - unless @drb_listener_running begin DRb.start_service("druby://127.0.0.1:0") rescue SocketError, Errno::EADDRNOTAVAIL DRb.start_service("druby://:0") end - @drb_listener_running = true end @spork_service ||= DRbObject.new_with_uri("druby://127.0.0.1:8989") end - def stop_spork - pid = File.read(spork_pid_file).to_i - Process.kill("SIGTERM",pid) - end - def start_spork if already_running?(spork_pid_file) puts "Killing old orphan spork instance" @@ -101,7 +103,13 @@ def start_spork running = spork_running? sleep 0.01 end + end + def stop_spork + pid = File.read(spork_pid_file).to_i + Process.kill("SIGTERM", pid) rescue nil end + end + end diff --git a/lib/demon/base.rb b/lib/demon/base.rb new file mode 100644 index 0000000000000..ad059c8878a95 --- /dev/null +++ b/lib/demon/base.rb @@ -0,0 +1,142 @@ +module Demon; end + +# intelligent fork based demonizer +class Demon::Base + + def self.start(count) + @demons ||= {} + count.times do |i| + (@demons["#{prefix}_#{i}"] ||= new(i)).start + end + end + + def self.stop + return unless @demons + @demons.values.each do |demon| + demon.stop + end + end + + def initialize(index) + @index = index + @pid = nil + @parent_pid = Process.pid + @monitor = nil + end + + def pid_file + "#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid" + end + + def stop + if @monitor + @monitor.kill + @monitor.join + @monitor = nil + end + + if @pid + Process.kill("HUP",@pid) + @pid = nil + end + end + + def start + if existing = already_running? + # should not happen ... so kill violently + Process.kill("TERM",existing) + end + + return if @pid + + if @pid = fork + write_pid_file + monitor_child + return + end + + monitor_parent + establish_app + after_fork + end + + def already_running? + if File.exists? pid_file + pid = File.read(pid_file).to_i + if alive?(pid) + return pid + end + end + + nil + end + + private + + def monitor_child + @monitor ||= Thread.new do + while true + sleep 5 + unless alive?(@pid) + STDERR.puts "#{@pid} died, restarting the process" + @pid = nil + start + end + end + end + end + + def write_pid_file + FileUtils.mkdir_p(Rails.root + "tmp/pids") + File.open(pid_file,'w') do |f| + f.write(@pid) + end + end + + def delete_pid_file + File.delete(pid_file) + end + + def monitor_parent + Thread.new do + while true + unless alive?(@parent_pid) + Process.kill "QUIT", Process.pid + end + sleep 1 + end + end + end + + def alive?(pid) + begin + Process.getpgid(pid) + true + rescue Errno::ESRCH + false + end + end + + def establish_app + ActiveRecord::Base.connection_handler.clear_active_connections! + ActiveRecord::Base.establish_connection + $redis.client.reconnect + Rails.cache.reconnect + MessageBus.after_fork + + Signal.trap("HUP") do + begin + delete_pid_file + ensure + exit + end + end + + # keep stuff simple for now + $stdout.reopen("/dev/null", "w") + $stderr.reopen("/dev/null", "w") + end + + def after_fork + end +end diff --git a/lib/demon/rails_autospec.rb b/lib/demon/rails_autospec.rb new file mode 100644 index 0000000000000..92d15a8fb2558 --- /dev/null +++ b/lib/demon/rails_autospec.rb @@ -0,0 +1,25 @@ +require "demon/base" + +class Demon::RailsAutospec < Demon::Base + + def self.prefix + "rails-autospec" + end + + private + + def after_fork + require "rack" + ENV["RAILS_ENV"] = "test" + Rack::Server.start( + :config => "config.ru", + :AccessLog => [], + :Port => ENV["TEST_SERVER_PORT"] || 60099, + ) + rescue => e + STDERR.puts e.message + STDERR.puts e.backtrace.join("\n") + exit 1 + end + +end diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index f4820f35c88f8..392128e1edfd3 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -1,148 +1,7 @@ -module Demon; end - -# intelligent fork based demonizer for sidekiq -class Demon::Base - - def self.start(count) - @demons ||= {} - count.times do |i| - (@demons["#{prefix}_#{i}"] ||= new(i)).start - end - end - - def self.stop - @demons.values.each do |demon| - demon.stop - end - end - - def initialize(index) - @index = index - @pid = nil - @parent_pid = Process.pid - @monitor = nil - end - - def pid_file - "#{Rails.root}/tmp/pids/#{self.class.prefix}_#{@index}.pid" - end - - def stop - if @monitor - @monitor.kill - @monitor.join - @monitor = nil - end - - if @pid - Process.kill("SIGHUP",@pid) - @pid = nil - end - end - - def start - if existing = already_running? - # should not happen ... so kill violently - Process.kill("SIGTERM",existing) - end - - return if @pid - - if @pid = fork - write_pid_file - monitor_child - return - end - - monitor_parent - establish_app - after_fork - end - - def already_running? - if File.exists? pid_file - pid = File.read(pid_file).to_i - if alive?(pid) - return pid - end - end - - nil - end - - private - - def monitor_child - @monitor ||= Thread.new do - while true - sleep 5 - unless alive?(@pid) - STDERR.puts "#{@pid} died, restarting sidekiq" - @pid = nil - start - end - end - end - end - - def write_pid_file - FileUtils.mkdir_p(Rails.root + "tmp/pids") - File.open(pid_file,'w') do |f| - f.write(@pid) - end - end - - def delete_pid_file - File.delete(pid_file) - end - - def monitor_parent - Thread.new do - while true - unless alive?(@parent_pid) - Process.kill "QUIT", Process.pid - end - sleep 1 - end - end - end - - def alive?(pid) - begin - Process.getpgid(pid) - true - rescue Errno::ESRCH - false - end - end - - def establish_app - - - ActiveRecord::Base.connection_handler.clear_active_connections! - ActiveRecord::Base.establish_connection - $redis.client.reconnect - Rails.cache.reconnect - MessageBus.after_fork - - Signal.trap("HUP") do - begin - delete_pid_file - ensure - exit - end - end - - # keep stuff simple for now - $stdout.reopen("/dev/null", "w") - # $stderr.reopen("/dev/null", "w") - end - - def after_fork - end -end +require "demon/base" class Demon::Sidekiq < Demon::Base + def self.prefix "sidekiq" end @@ -151,18 +10,15 @@ def self.prefix def after_fork require 'sidekiq/cli' - begin - # Reload initializer cause it needs to run after sidekiq/cli - # was required - load Rails.root + "config/initializers/sidekiq.rb" - cli = Sidekiq::CLI.instance - cli.parse([]) - cli.run - rescue => e - STDERR.puts e.message - STDERR.puts e.backtrace.join("\n") - exit 1 - end - + # Reload initializer cause it needs to run after sidekiq/cli was required + load Rails.root + "config/initializers/sidekiq.rb" + cli = Sidekiq::CLI.instance + cli.parse([]) + cli.run + rescue => e + STDERR.puts e.message + STDERR.puts e.backtrace.join("\n") + exit 1 end + end diff --git a/lib/tasks/autospec.rake b/lib/tasks/autospec.rake index c4ca2008c30dd..43455ac850c9c 100644 --- a/lib/tasks/autospec.rake +++ b/lib/tasks/autospec.rake @@ -4,22 +4,17 @@ desc "Run all specs automatically as needed" task "autospec" => :environment do + require 'autospec/manager' - if RUBY_PLATFORM.include?('linux') - require 'rb-inotify' - end - - require 'listen' - - puts "If file watching is not working you can force polling with: bundle exec rake autospec p l=3" - require 'autospec/runner' - - force_polling = ARGV.any?{|a| a == "p" || a == "polling"} - latency = ((ARGV.find{|a| a =~ /l=|latency=/}||"").split("=")[1] || 3).to_i + force_polling = ARGV.any?{ |a| a == "p" || a == "polling" } + latency = ((ARGV.find{ |a| a =~ /l=|latency=/ } || "").split("=")[1] || 3).to_i if force_polling - puts "polling has been forced (slower) checking every #{latency} #{"second".pluralize(latency)}" + puts "Polling has been forced (slower) - checking every #{latency} #{"second".pluralize(latency)}" + else + puts "If file watching is not working, you can force polling with: bundle exec rake autospec p l=3" end - Autospec::Runner.run(force_polling: force_polling, latency: latency) + Autospec::Manager.run(force_polling: force_polling, latency: latency) + end diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index aac9a5228b35b..26c24722bfb30 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -28,7 +28,6 @@ end it "has a name" do - puts json[:name] json[:name].should be_blank end end diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js index 469c770591915..3f0272f39e18b 100644 --- a/vendor/assets/javascripts/run-qunit.js +++ b/vendor/assets/javascripts/run-qunit.js @@ -19,16 +19,14 @@ page.onConsoleMessage = function(msg) { if (msg.slice(0,8) === 'WARNING:') { return; } if (msg.slice(0,6) === 'DEBUG:') { return; } - // Hack to access the print method - // If there's a better way to do this, please change - if (msg.slice(0,6) === 'PRINT:') { - print(msg.slice(7)); - return; - } - console.log(msg); }; +page.onCallback = function (message) { + // forward the message to the standard output + system.stdout.write(message); +}; + page.open(args[0], function(status) { if (status !== 'success') { console.error("Unable to access network"); @@ -80,9 +78,9 @@ function logQUnit() { var msg = " Test Failed: " + context.name + assertionErrors.join(" "); testErrors.push(msg); assertionErrors = []; - console.log('PRINT: F'); + window.callPhantom('F'); } else { - console.log('PRINT: .'); + window.callPhantom('.'); } }); From a65a063a96f591b6d147196a43a1e80dbb7e7fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Sat, 2 Nov 2013 00:22:25 +0100 Subject: [PATCH 07/25] fix run-qunit --- vendor/assets/javascripts/run-qunit.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js index 3f0272f39e18b..6a0dd248785f4 100644 --- a/vendor/assets/javascripts/run-qunit.js +++ b/vendor/assets/javascripts/run-qunit.js @@ -8,12 +8,8 @@ if (args.length < 1 || args.length > 2) { phantom.exit(1); } -var fs = require('fs'); -function print(str) { - fs.write('/dev/stdout', str, 'w'); -} - -var page = require('webpage').create(); +var system = require("system"), + page = require('webpage').create(); page.onConsoleMessage = function(msg) { if (msg.slice(0,8) === 'WARNING:') { return; } From 8ff35d4b10980bc94db8d485caf9dea0cbcc68df Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 2 Nov 2013 10:25:43 +1100 Subject: [PATCH 08/25] automatically make developers admins on account creation, this solves the user #1 problem you can simply set the DEVELOPER_EMAILS to a comma delimited list and the users will be auto admined --- config/environments/production.rb.sample | 4 +++- lib/auth/default_current_user_provider.rb | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/config/environments/production.rb.sample b/config/environments/production.rb.sample index 3b19c592f9404..8257958fa4894 100644 --- a/config/environments/production.rb.sample +++ b/config/environments/production.rb.sample @@ -85,6 +85,8 @@ Discourse::Application.configure do # a comma delimited list of emails your devs have # developers have god like rights and may impersonate anyone in the system # normal admins may only impersonate other moderators (not admins) - config.developer_emails = [] + if emails = ENV["DEVELOPER_EMAILS"] + config.developer_emails = emails.split(",") + end end diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 98430e3207247..14341fcccb04f 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -63,9 +63,19 @@ def log_on_user(user, session, cookies) user.save! end cookies.permanent[TOKEN_COOKIE] = { value: user.auth_token, httponly: true } + make_developer_admin(user) @env[CURRENT_USER_KEY] = user end + def make_developer_admin(user) + if user.active? && + !user.admin && + Rails.configuration.respond_to?(:developer_emails) && + Rails.configuration.developer_emails.include?(user.email) + user.update_column(:admin, true) + end + end + def log_off_user(session, cookies) cookies[TOKEN_COOKIE] = nil end From a7f53847c1ec0eb576ba5ecefb713732b8709d76 Mon Sep 17 00:00:00 2001 From: ofGEEK Date: Sat, 2 Nov 2013 22:53:46 +0800 Subject: [PATCH 09/25] Update client.zh_CN.yml --- config/locales/client.zh_CN.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 26fc3c21f71a9..fce4dca3552d0 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -657,8 +657,7 @@ zh_CN: auto_close_notice: "本主题将在%{timeLeft}后自动关闭" auto_close_title: '自动关闭设置' auto_close_save: "保存" - auto_close_cancel: "取消" - auto_close_remove: "不自动关闭该主题" + auto_close_remove: "不要自动关闭该主题" progress: title: 主题进度 @@ -1075,10 +1074,10 @@ zh_CN: # This section is exported to the javascript for i18n in the admin section admin_js: - type_to_filter: "输入过滤条件……" + filter: "过滤器" admin: - title: '论道 管理' + title: 'Discourse管理' moderator: '版主' dashboard: @@ -1260,6 +1259,8 @@ zh_CN: change_site_setting: "更改站点设置" change_site_customization: "更改站点自定义" delete_site_customization: "删除站点自定义" + ban_user: "禁止用户" + unban_user: "解禁用户" screened_emails: title: "被屏蔽的邮件地址" description: "当有人试图用以下邮件地址注册时,将受到阻止或其它系统操作。" @@ -1331,7 +1332,11 @@ zh_CN: user: ban_failed: "禁止此用户时发生了错误 {{error}}" unban_failed: "解禁此用户时发生了错误 {{error}}" - ban_duration: "你计划禁止该用户多久?(天)" + ban_duration: "你计划禁止该用户多久?" + ban_duration_units: "(天)" + ban_reason_label: "为什么禁止该用户?当其尝试登入时,会看到这条理由。" + ban_reason: "禁止的理由" + banned_by: "禁止操作者:" delete_all_posts: "删除所有帖子" delete_all_posts_confirm: "你将删除 %{posts} 个帖子和 %{topics} 个主题,确认吗?" ban: "禁止" @@ -1388,7 +1393,8 @@ zh_CN: deactivate_explanation: "已停用的用户必须重新验证他们的电子邮件。" banned_explanation: "被禁止的用户无法登录。" block_explanation: "被封禁的用户不能发表主题或者评论。" - trust_level_change_failed: "改变用户等级时出现了一个问题," + trust_level_change_failed: "改变用户等级时出现了一个问题。" + ban_modal_title: "禁止用户" site_content: none: "选择内容类型以开始编辑。" From 855ee3b43da817c2e1700818dd4d6bca92203412 Mon Sep 17 00:00:00 2001 From: Vikhyat Korrapati Date: Sun, 3 Nov 2013 10:41:38 +0530 Subject: [PATCH 10/25] Fix ActiveRecord::Associations::CollectionProxy serialization in Rails 4. --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5a7dad77f09e6..2a339818d260e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -134,7 +134,7 @@ def guardian def serialize_data(obj, serializer, opts={}) # If it's an array, apply the serializer as an each_serializer to the elements serializer_opts = {scope: guardian}.merge!(opts) - if obj.is_a?(Array) + if obj.is_a?(Array) or obj.is_a?(ActiveRecord::Associations::CollectionProxy) serializer_opts[:each_serializer] = serializer ActiveModel::ArraySerializer.new(obj, serializer_opts).as_json else From 895d801669137088440bd840a7a8d52d604b22bc Mon Sep 17 00:00:00 2001 From: Vipul A M Date: Sun, 3 Nov 2013 15:36:59 +0530 Subject: [PATCH 11/25] Dont call `SiteSetting.uncategorized_category_id` twice --- spec/spec_helper.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5d9db38515845..e95bab803c446 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -77,8 +77,6 @@ def log_off_user(session,cookies) config.before(:all) do DiscoursePluginRegistry.clear - uncat_id = SiteSetting.uncategorized_category_id - Discourse.current_user_provider = TestCurrentUserProvider # a bit odd, but this setting is actually preloaded From 866a7057add4c1ecf779d8eb718fd6612746aab6 Mon Sep 17 00:00:00 2001 From: Sander Datema Date: Sun, 3 Nov 2013 22:22:40 +0100 Subject: [PATCH 12/25] Update server.nl.yml --- config/locales/server.nl.yml | 134 +++++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 20 deletions(-) diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index edac72a3f6780..99d860334a8e1 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -97,7 +97,7 @@ nl: Voor meer informatie, [bekijk onze Veel Gestelde Vragen](/faq). Deze tekst zal alleen verschijnen bij je eerste %{education_posts_text}. 'new-reply': | - Welkom op %{site_name} — **bedankt voor het deelnemen aan de conversatie!** + Welkom op %{site_name} — **bedankt voor je bijdrage!** - Geeft je reactie een nuttige bijdrage aan de conversatie, hoe klein dan ook? @@ -107,6 +107,30 @@ nl: Voor meer informatie, [bekijk onze Veel Gestelde Vragen](/faq). Deze tekst zal alleen verschijnen bij je eerste %{education_posts_text}. + avatar: | + ### Wat dacht je van een nieuwe afbeelding voor je account? + + Je hebt een aantal berichten en reacties geplaatst, maar je avatar is niet zo uniek als jij bent -- het is dezelfde avatar als alle andere nieuwe gebruikers hebben. + + Heb je al overwogen **[om naar je profielpagina te gaan](%{profile_path})** om een nieuwe afbeelding van jou te uploaden? + + Het is makkelijker om conversaties te volgen en interessante mensen te vinden als iedereen een unieke avatar heeft! + + sequential_replies: | + ### Overweeg om op meerdere berichten ineens te reageren + + In plaatst van een reactie per bericht te plaatsten, kan je ook meerdere berichten ineens citeren en @namen noemen in je reactie. + + Je kan je vorige reactie wijzigen en er een citaat aan toevoegen door de betreffende tekst te selecteren en op de citeer knop te drukken die dan verschijnt. + + Het is voor iedereen makkelijker om topics te lezen met minder losstaande reacties. + + dominating_topic: | + ### Laat anderen meedoen in de conversatie + + Deze topic is duidelijk belangrijk voor je – meer dan %{percent}% van de reacties zijn van jou. + + Hou je in de gaten dat anderen ook de ruimte krijgen om hun visie te geven? activerecord: attributes: @@ -114,6 +138,8 @@ nl: name: Naam categorie post: raw: Inhoud + user: + ip_address: "" errors: messages: is_invalid: "is ongeldig; probeer wat uitgebreider te zijn" @@ -123,6 +149,10 @@ nl: attributes: archetype: cant_send_pm: "Sorry, je kan geen privébericht sturen naar deze persoon." + user: + attributes: + ip_address: + signup_not_allowed: Inschrijven vanaf dit account is niet toegestaan. user_profile: no_info_me: "
Het Over Mij-profielveld is nog leeg, zou je deze willen invullen?
" @@ -132,7 +162,9 @@ nl: topic_prefix: "Beschrijving voor categorie %{category}" replace_paragraph: "[Vervang deze eerste regel met een korte omschrijving van je nieuwe categorie. Deze regel verschijnt in het selectiemenu als iemand een categorie kiest, dus hou het kort (max. 200 tekens).]" post_template: "%{replace_paragraph}\n\nGebruik de volgende alinea's voor een lange omschrijving en om wat verwachtingen en regels van deze categorie uit te leggen.\n\nZaken waar je het over kan hebben in de reacties hieronder:\n\n- Waar is deze categorie voor? Waarom zouden mensen deze categorie moeten kiezen voor hun topic?\n\n- Waarin verschilt deze categorie van de andere categorien die we al hebben?\n\n- Hebben we deze categorie echt nodig?\n\n- Moeten we deze categorie samenvoegen met een andere categorie, of juist opsplitsen?\n" - + errors: + self_parent: "Een subcategorie kan niet onder zichzelf hangen." + depth: "Je kan een subcategorie niet onder een andere subcategorie hangen." trust_levels: newuser: title: nieuw lid @@ -143,7 +175,7 @@ nl: leader: title: leider elder: - title: stamoudste + title: emiritus change_failed_explanation: "Je probeerde %{user_name} te degraderen naar '%{new_trust_level}'. Echter, het trust level is al '%{current_trust_level}'. %{user_name} blijft op trust level '%{current_trust_level}'" @@ -263,7 +295,7 @@ nl: long_form: heeft dit als spam gemeld inappropriate: title: Ongepast - description: 'Dit bericht bevat inhoud dat een persoon met gezond verstand als beledigend, discriminerend of kwetsend kan ervaren.' + description: 'Dit bericht bevat inhoud dat iemand als beledigend, discriminerend of kwetsend kan ervaren. Ook kan het een overtreding van de regels zijn.' long_form: heeft dit als ongepast gemeld notify_user: title: "Licht {{username}} in" @@ -389,6 +421,7 @@ nl: dashboard: rails_env_warning: "Je server draait in %{env} modus." + ruby_version_warning: Je gebruikt een versie van Ruby 2.0.0 met problemen. Upgrade naar patch level 247 of later. host_names_warning: "Het bestand config/database.yml heeft localhost als standaard hostname. Werk dat bij naar de hostname van je site." gc_warning: 'Je server gebruikt de standaard ruby garbage collection instellingen, en daarmee krijg je niet de beste prestaties. Lees deze topic over instellingen voor prestaties: Tuning Ruby and Rails for Discourse.' sidekiq_warning: 'Sidekiq draait niet. Veel taken, zoals het versturen van e-mails, worden asynchroon uitgevoerd door sidekiq. Zorg ervoor dat er altijd een sidekiq process draait. Hier is meer informatie over sidekiq.' @@ -399,12 +432,13 @@ nl: twitter_config_warning: 'De server is geconfigureerd om registratie en inloggen via Twitter mogelijk te maken (enable_twitter_logins), maar er zijn geen key en secret waarden opgegeven. Ga naar de Instellingen en vul de waarden in. Zie deze uitleg voor meer informatie.' github_config_warning: 'De server is geconfigureerd om registratie en inloggen via Github mogelijk te maken (enable_github_logins), maar er zijn geen client id en secret waarden opgegeven. Ga naar de Instellingen en vul de waarden in. Zie deze uitleg voor meer informatie.' s3_config_warning: 'De server is geconfigureerd om bestanden naar S3 te uploaden, maar tenminste een van de volgende instellingen is niet opgegeven: s3_access_key_id, s3_secret_access_key of s3_upload_bucket. Ga naar de Instellingen en vul de waarden in. Zie "How to set up image uploads to S3?" voor meer informatie.' - image_magick_warning: 'De server is geconfigureerd om thumbnails te maken van grote afbeeldingen, maar ImageMagick is niet geïnstalleerd. Installeer ImageMagick met je favoriete package manager of ga naar om de laatste release te downloaden.' + image_magick_warning: 'De server is geconfigureerd om thumbnails te maken van grote afbeeldingen, maar ImageMagick is niet geïnstalleerd. Installeer ImageMagick met je favoriete package manager of download de laatste release.' failing_emails_warning: 'Er zijn %{num_failed_jobs} mislukte emailtaken. Check of de instellingen voor config.action_mailer in het bestand config/environments/production.rb kloppen. Bekijk alle mislukte Sidekiq jobs.' default_logo_warning: "Je hebt nog niet een eigen logo ingesteld voor je site. Werk logo_url, logo_small_url, en favicon_url bij in de Instellingen." contact_email_missing: "Je hebt nog geen contactadres opgegeven voor je site. Werk contact_email bij in de Instellingen." contact_email_invalid: "Je hebt een ongeldig contactadres opgegeven voor je site. Werk contact_email bij in de Instellingen." title_nag: "Je hebt nog geen title ingesteld voor je site. Geef een titel voor je site op in de Instellingen." + site_description_missing: "Er is nog geen omschrijving van deze site. Schrijf een korte omschrijving in de Instellingen" consumer_email_warning: "Je site is ingesteld om Gmail te gebruiken voor het versturen van mails. Gmail heeft limieten voor het aantal mails dat je kan versturen. Overweeg om een andere e-mailprovider te gebruiken om er zeker van te zijn dat mails aankomen." access_password_removal: "Je site gebruikte een toegangswachtwoord (access_password) setting, maar die optie is uit Discourse verwijderd. De login_required en must_approve_users instellingen zijn er voor in de plaats gekomen. Je kan ze in de Instellingen aanpassen. Zorg er voor dat je gebruikers op de wachtlijst accepteert. (Dit bericht verdwijnt na twee dagen.)" site_contact_username_warning: "De instelling site_contact_username is leeg. Werk deze bij in de Instellingen. Stel het in op de gebruikersnaam van een admin die als afzender van de systeemberichten zal worden wordt gebruikt." @@ -461,6 +495,7 @@ nl: discourse_org_access_key: "De toegangscode voor het discourse.org nickname-register" educate_until_posts: Laat een popup zien totdat een lid dit aantal berichten geplaatst heeft title: "Titel van deze website, wordt gebruikt in de paginatitel en elders" + site_description: Omschrijf dit forum in een zin. Deze wordt getoond in de meta tag. contact_email: E-mailadres van een contactpersoon van deze site. Belangrijke updates van Discourse.org worden naar dit adres gestuurd. company_full_name: "De volledige naam van het bedrijf dat deze site draait. Wordt gebruikt in juridische delen van de site, zoals /tos" company_short_name: "De korte naam van het bedrijf dat deze site draait. Wordt gebruikt in juridische delen van de site, zoals /tos" @@ -470,7 +505,8 @@ nl: ninja_edit_window: "Hoe snel je een aanpassing kan maken zonder dat er een nieuwe versie wordt opgeslagen, in seconden." edit_history_visible_to_public: "Iedereen mag eerdere versies van een bericht zien. Wanneer aangevinkt kan alleen de staf de eerdere versies van een bericht zien." delete_removed_posts_after: "Na dit aantal uren zal een door een gebruiker verwijderd bericht ook echt helemaal verwijderd worden (in plaats van alleen verborgen)." - max_image_width: Maximale breedte voor een afbeelding in een bericht + max_image_width: Maximale breedte van een afbeelding in een bericht + max_image_height: Maximale hoogte van een afbeelding in een bericht category_featured_topics: Aantal topics dat wordt weergegeven in de categorielijst add_rel_nofollow_to_user_content: "Voeg 'rel nofollow' toe aan alle leden-content behalve voor interne links (inclusief parent domeinen). NB: Als je dit verandert, moet je ook alle 'baked markdown' updaten met \"rake posts:rebake\"" exclude_rel_nofollow_domains: "Een kommagescheiden lijst van domeinen waar 'nofollow' niet is toegevoegd (voorbeelddomein.com zal automatisch sub.voorbeelddomein.com toestaan)." @@ -504,9 +540,14 @@ nl: flags_required_to_hide_post: "Berichten zullen automatisch worden verborgen zodra het aantal meldingen maximaal dit aantal is (0 voor nooit)" cooldown_minutes_after_hiding_posts: "Hoeveel minuten moet iemand wachten voordat zij hun bericht kunnen wijzigen nadat het is verborgen door meldingen" + + max_topics_in_first_day: "Het maximum aantal topics dat een gebruiker mag maken tijdens hun eerste dag op de site" + max_replies_in_first_day: "Het maximum aantal reacties dat een gebruiker mag schrijven tijdens hun eerste dag op de site" + num_flags_to_block_new_user: "Als de berichten van een nieuwe gebruiker dit aantal spammarkeringen krijgen van (n) verschillende gebruikers, verberg dan alle berichten van deze gebruiker en houdt nieuwe berichten tegen. 0 om deze mogelijkheid uit te zetten." num_users_to_block_new_user: "Als de berichten van een nieuwe gebruiker (x) spammarkeringen krijgen van andere gebruikers, verberg dan alle berichten van deze gebruiker en houdt nieuwe berichten tegen. 0 om deze mogelijkheid uit te zetten." - + notify_mods_when_user_blocked: "Als een gebruiker automatisch geblokkeerd is, stuur dan een bericht naar alle moderatoren." + flag_sockpuppets: "Als een nieuwe gebruiker (bijv. geregistreerd in de afgelopen 24 uur) reageert op de topic van een andere nieuwe gebruiker, maar wel van hetzelfde IP-adres dan, worden beide berichten automatisch gemarkeerd als spam." traditional_markdown_linebreaks: "Gebruik traditionele regeleinden in Markdown, gebruik 2 spaties voor een nieuw regeleinde" post_undo_action_window_mins: "Het tijdsbestek waarin iemand een actie binnen een bericht kan terugdraaien (zoals 'vind ik leuk')" @@ -530,15 +571,16 @@ nl: email_domains_blacklist: "Een lijst met e-maildomeinen gescheiden door een |-teken van domeinen die niet worden toegestaan. Voorbeeld: mailinator.com|trashmail.net" email_domains_whitelist: "Een lijst met e-maildomeinen gescheiden door een |-teken van domeinen die wel worden toegestaan. N.B.: als je hier iets opgeeft, zijn andere domeinen NIET toegestaan." version_checks: "Ping de Discourse-hub voor versieupdates and laat versiemeldingen zien in het /admin dashboard" + new_version_emails: Stuur een e-mail naar het contact-email adres als er een nieuwe versie beschikbaar is. - port: "Mocht je een specifieke poort willen toewijzen aan de URL. Handig in ontwikkelaars-modus. Laat leeg voor geen toewijzing." - force_hostname: "Mocht je een specifieke hostname willen toewijzen. Handig in ontwikkelaars-modus. Laat leeg voor geen toewijzing." + port: "DEVELOPER ONLY! WARNING! Mocht je een specifieke poort willen toewijzen aan de URL. Handig in ontwikkelaars-modus. Laat leeg voor geen toewijzing." + force_hostname: "DEVELOPER ONLY! WARNING! Mocht je een specifieke hostname willen toewijzen. Handig in ontwikkelaars-modus. Laat leeg voor geen toewijzing." invite_expiry_days: "Hoe lang uitnodigingscodes geldig blijven (in dagen)." # TODO: perhaps we need a way of protecting these settings for hosted solution, global settings ... - invite_only: "Registratie is gesloten, alleen toegang op uitnodiging." + invite_only: "Registratie is niet mogelijk, alleen toegang op uitnodiging." login_required: Inloggen vereist om berichten te kunnen lezen @@ -583,6 +625,8 @@ nl: suggested_topics: Het aantal aanbevolen topics dat is weergegeven aan de onderkant van een topic + clean_up_uploads: "Verwijder weesbestanden om illegale hosting te voorkomen. LET OP: maak een backup van je /uploads directory voordat je deze instelling activeert." + uploads_grace_period_in_hours: Na hoeveel uur een weesbestand verwijderd wordt. enable_s3_uploads: Of we uploads op Amazon S3 willen zetten of niet s3_upload_bucket: "De 'bucket' waarin we onze uploads naar Amazon S3 willen zetten" s3_access_key_id: "De Amazon S3 access key id dat wordt gebruikt om afbeeldingen te uploaden" @@ -606,6 +650,8 @@ nl: regular_requires_likes_given: "Hoeveel keer een nieuw lid berichten leuk moet vinden voordat hij gepromoveerd wordt tot regulier lid (trust level 2)" regular_requires_topic_reply_count: "Op hoeveel topics een nieuw lid moet reageren voordat hij gepromoveerd wordt tot regulier lid (trust level 2)" + min_trust_to_create_topic: "Het minimale trust level dat nodig is om een topic te mogen maken." + newuser_max_links: Hoeveel links een nieuw lid in een bericht kan plaatsen newuser_max_images: Hoeveel afbeeldingen een nieuw lid in een bericht kan plaatsen newuser_max_images: Hoeveel bestanden een nieuw lid in een bericht kan plaatsen @@ -627,6 +673,8 @@ nl: min_body_similar_length: De minimale lengte die de inhoud van een bericht moet hebben voordat er wordt gezocht naar vergelijkbare topics category_colors: "Een lijst, gescheiden door een pipe (|), van hexadecimal kleurwaardes die gebruikt kunnen worden voor categorien" + enable_wide_category_list: "Activeer traditionele volle breedte, non-tiling, categorielijst" + max_image_size_kb: "De maximale afbeeldingsgrootte die we toestaan voor uploads, in kB. Zorg er voor dat deze limiet ook ingesteld is in nginx (client_max_body_size) / apache of een proxy." max_attachment_size_kb: "De maximale bestandsgrootte die we toestaan voor uploads, in kB. Zorg er voor dat deze limiet ook ingesteld is in nginx (client_max_body_size) / apache of een proxy." authorized_extensions: "Een met pipes (|) gescheiden lijst van bestandsextensies die mogen worden geupload" @@ -659,9 +707,24 @@ nl: relative_date_duration: "Na hoeveel dagen de datum van een bericht relatief zijn in plaats van absoluut.Voorbeelden: relatieve datum: 7d, absolute datum: 20 feb" delete_user_max_age: "Na hoeveel dagen na de inschrijving mag een admin een gebruiker nog verwijderen." delete_all_posts_max: "Het maximaal aantal berichten dat ineens verwijderd kan worden met de 'Verwijder alle berichten'-knop. Als een gebruiker meer berichten heeft, kunnen de berichten niet in een keer verwijderd worden en kan de gebruiker dus niet verwijderd worden." - username_change_period: "The number of days after registration that accounts can change their username." + username_change_period: "The number of days after registration that accounts can change their username (0 om wijziging niet toe te staan)." + email_editable: Gebruikers mogen hun e-mailadres na registratie nog wijzigen. allow_uploaded_avatars: "Sta toe dat avatars geupload kunnen worden" + allow_animated_avatars: "Gebruikers mogen een animated GIF gebruiken als avatar. LET OP: draai de rake task avatars:regenerate nadat je deze instelling hebt geactiveerd." + default_digest_email_frequency: "Hoe vaak ontvangen gebruikers standaard de digestmails. Ze kunnen dit in hun eigen instellingen nog aanpassen." + + detect_custom_avatars: "Check of gebruikers een eigen avatar geupload hebben" + max_daily_gravatar_crawls: "Het maximum aantal keren per dag dat Discourse checkt of er nieuwe Gravatars zijn" + + sequential_replies_threshold: "Het aantal reacties dat een gebruiker achter elkaar moet plaatsen in een topic om een melding te krijgen" + + enable_mobile_theme: "Mobiele apparaten gebruiken een mobiel-vriendelijke theme met de mogelijkheid te schakelen naar de volledige site. Schakel deze optie uit als je een eigen stylesheet wil gebruiken die volledig responsive is." + + dominating_topic_minimum_percent: "Vanaf welk percentage berichten dat een gebruiker plaatst in een topic vinden we de gebruiker dominant?" + + enable_names: "Gebruikers mogen hun volledige naam laten zien" + display_name_on_posts: "Laat de volledige naam van een gebruiker ook bij zijn berichten zien" notification_types: mentioned: "%{display_username} heeft je genoemd in %{link}" @@ -717,6 +780,7 @@ nl: activate_email: "Je bent er bijna! We hebben een activatiemail verstuurd naar %{email}. Volg de instructies in de mail om je account te activeren." not_activated: "Je kan nog niet inloggen. We hebben je een activatiemail gestuurd. Volg de instructies in de mail om je account te activeren." banned: "Je kan tot %{date} niet inloggen." + banned_with_reason: "Je kan tot %{date} niet inloggen. Je hebt een ban om de volgende reden: %{reden}" errors: "%{errors}" not_available: "Niet beschikbaar. Probeer %{suggestion}?" something_already_taken: "Er ging iets mis, misschien zijn de gebruikersnaam en/of e-mailadres al in gebruik? Gebruik dan de 'wachtwoord vergeten' link" @@ -727,13 +791,15 @@ nl: username: short: "moet langer zijn dan %{min} tekens" long: "moet korter zijn dan %{max} tekens" - characters: "mag alleen nummers en letters bevatten" - unique: "moet uniek zijn" - blank: "mag niet leeg zijn" + characters: mag alleen nummers en letters bevatten + unique: moet uniek zijn + blank: mag niet leeg zijn must_begin_with_alphanumeric: "moet met een letter of nummer beginnen" email: not_allowed: "is niet toegestaan vanaf die e-mailprovider. Gebruik een ander e-mailadres." - blocked: "is niet toegestaan." + blocked: is niet toegestaan. + ip_address: + blocked: is geblokkeerd. invite_mailer: subject_template: "[%{site_name}] %{invitee_name} heeft je uitgenodigd om op een privé-bericht te reageren op %{site_name}" @@ -791,6 +857,17 @@ nl: Er zou een unsubscribe footer op elke email die je verstuurt moeten zitten zodat men direct kan afmelden mocht men deze mails niet willen ontvangen. Dus laten we er eentje opzetten! + new_version_mailer: + subject_template: "[%{site_name}] Nieuwe updates beschikbaar" + text_body_template: | + Er is een nieuwe versie van Discourse beschikbaar. + + **Nieuwe versie: %{new_version}** + + Jouw versie: %{installed_version} + + Werk zo snel mogelijk bij om de laatste oplossingen in mogelijkheden te krijgen. + system_messages: post_hidden: subject_template: "Bericht van %{site_name}: je bericht is verborgen wegens meldingen uit de community" @@ -912,19 +989,23 @@ nl: Kijk voor verdere uitleg in de [FAQ](%{base_url}/faq). user_automatically_blocked: - subject_template: "Nieuwe gebruiker %{username} was automatisch geblokkeerd" + subject_template: "Nieuwe gebruiker %{username} is door meldingen geblokkeerd" text_body_template: | - Dit is een automatisch bericht om je te informeren dat de nieuwe gebruiker [%{username}](%{user_url}) automatisch geblokkeerd is, omdat meerdere gebruikers de berichten van %{username} gemarkeerd hebben. + Dit is een automatisch bericht om je te informeren dat de nieuwe gebruiker [%{username}](%{base_url}%{user_url}) automatisch geblokkeerd is, omdat meerdere gebruikers de berichten van %{username} gemarkeerd hebben. Kijk hier voor [de markeringen](/admin/flags). Als %{username} ten onrechte geblokkeerd is, klik dan op de deblokkeerdknop op [de beheerpagina van deze gebruiker](%{user_url}). + De drempel hiervoor kan ingesteld worden met de `block_new_user` instelling. + spam_post_blocked: - subject_template: "Spam gedetecteerd in een bericht van %{username}" + subject_template: "Berichten van nieuwe gebruiker %{username} geblokkeerd door herhaalde links" text_body_template: | - Dit is een automatisch bericht van %{site_name} om je te informeren dat [%{username}](%{user_url}) probeerde een bericht te plaatsen met links, maar gebaseerd op newuser_spam_host_threshold is dit gezien als spam. + Dit is een automatisch bericht van %{site_name} om je te informeren dat de nieuwe gebruiker [%{username}](%{base_url}%{user_url}) probeerde meerdere berichten te plaatsen met links naar %{domains}, maar deze berichten zijn geblokkeerd om spam tegen te gaan. De gebruiker kan nog steeds nieuwe berichten plaatsen die niet linken naar %{domains}. Bekijk de gebruiker [hier](%{user_url}). + De drempel hiervoor kan ingesteld worden met de `newuser_spam_host_threshold` instelling. + unblocked: subject_template: Account gedeblokkeerd text_body_template: | @@ -934,6 +1015,15 @@ nl: Je kunt weer berichten plaatsen + pending_users_reminder: + subject_template: + one: "1 gebruiker wacht op goedkeuring" + other: "%{count} gebruikers wachten op goedkeuring" + text_body_template: | + Er zijn nieuwe gebruikers die zich hebben ingeschreven en wachten op goedkeuring (of afwijzing) voor zij het forum op kunnen. + + [Beoordeel deze gebruikers in het admingedeelte](/admin/users/list/pending). + unsubscribe_link: "Om deze e-mails niet langer willen ontvangen, ga naar [je gebruikersinstellingen](%{user_preferences_url})." user_notifications: @@ -1000,7 +1090,7 @@ nl: new_activity: "Nieuwe reacties op je topics en berichten:" top_topics: "Veel besproken topics:" new_topics: "Nieuwe topics:" - unsubscribe: "Deze samenvatting wordt door %{site_link} verstuurd als we je 7 dagen hebben gemist op onze site.\nMocht je dit uit willen zetten, of je e-mailvoorkeur willen veranderen, %{unsubscribe_link}." + unsubscribe: "Deze samenvatting wordt door %{site_link} verstuurd als we je 7 dagen hebben gemist op onze site. Mocht je dit uit willen zetten, of je e-mailvoorkeur willen veranderen, %{unsubscribe_link}." click_here: klik hier from: "%{site_name} Digest" read_more: Lees verder @@ -1083,3 +1173,7 @@ nl: fetch_failure: "Er ging iets mis bij het opvragen van de afbeelding." unknown_image_type: "Het bestand dat je wil uploaden is geen afbeelding." size_not_found: "Het is niet gelukt de afmetingen van de afbeelding te bepalen. Misschien is het bestand corrupt?" + + flag_reason: + sockpuppet: "Een nieuwe gebruiker maakte een nieuwe topic en een andere gebruiker reageerde - vanaf hetzelfde IP-adres. Zie de flag_sockpuppets instelling." + spam_hosts: "Deze gebruiker probeerde om meerdere berichten met links naar hetzelfde domein te plaatsen. Zie de newuser_spam_host_threshold instelling." From 57c0f5ee4cfeea6f184e37454cfe8a9a8cf508c7 Mon Sep 17 00:00:00 2001 From: Sander Datema Date: Sun, 3 Nov 2013 22:24:26 +0100 Subject: [PATCH 13/25] Update client.nl.yml --- config/locales/client.nl.yml | 147 +++++++++++++++++++++++++++++++---- 1 file changed, 133 insertions(+), 14 deletions(-) diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 5e64204e5c5df..6da8a354acd42 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -75,7 +75,7 @@ nl: other: "%{count} dagen geleden" share: topic: Deel een link naar deze topic - post: Deel een link naar dit bericht + post: "Deel een link naar bericht #%{postNumber}" close: sluit twitter: deel deze link op Twitter facebook: deel deze link op Facebook @@ -92,18 +92,28 @@ nl: log_in: Log in age: Leeftijd last_post: Laatste bericht + joined: Lid sinds admin_title: Beheer flags_title: Meldingen show_more: meer... links: Links faq: FAQ privacy_policy: "Privacy Policy" + mobile_view: Mobiele versie + desktop_versie: Desktopversie you: Jij or: of now: zonet read_more: lees verder more: Meer less: Minder + never: nooit + daily: dagelijks + weekly: wekelijks + every_two_weeks: elke twee weken + character_count: + one: "{{count}} teken" + other: "{{count}} tekens" in_n_seconds: one: "over 1 seconde" @@ -136,6 +146,10 @@ nl: saving: Wordt opgeslagen... saved: Opgeslagen! + upload: Upload + uploading: Uploaden... + uploaded: Geupload! + choose_topic: none_found: Geen topics gevonden. title: @@ -173,9 +187,21 @@ nl: "12": "Verzonden items" "13": "Inbox" + categories: + all: alle categoriëen + only_category: "only {{categoryName}}" + category: Categorie + posts: Berichten + topics: Topics + latest: Laatste + latest_by: Laatste door + toggle_ordering: schakel sorteermethode + subcategories: "Subcategoriëen:" + user: said: "{{username}} zei:" profile: Profiel + show_profile: Bekijk profiel mute: Negeer edit: Wijzig voorkeuren download_archive: download een archief van mijn berichten @@ -193,11 +219,18 @@ nl: change: verander moderator: "{{user}} is een moderator" admin: "{{user}} is een admin" + deleted: (verwijderd) + + messages: + all: Alle + mine: Mijn + unread: Ongelezen change_password: success: (e-mail verzonden) in_progress: (e-mail wordt verzonden) error: (fout) + action: Stuur wachtwoord-reset-mail change_about: title: Wijzig bio @@ -215,6 +248,15 @@ nl: error: "Het veranderen van je e-mailadres is mislukt. Wellicht is deze al in gebruik?" success: "We hebben een mail gestuurd naar dat adres. Volg de bevestigingsinstructies in die mail." + change_avatar: + title: Wijzig je avatar + gravatar: "Gravatar, gebaseerd op" + gravatar_title: Verander je avatar op Gravatars website + uploaded_avatar: Eigen afbeelding + uploaded_avatar_empty: Voeg een eigen afbeelding toe + upload_title: Upload je afbeelding + image_is_not_a_square: "Let op: we hebben je afbeelding bijgesneden omdat het geen vierkant is." + email: title: E-mail instructions: Je e-mail adres zal nooit publieklijk zichtbaar zijn. @@ -259,6 +301,7 @@ nl: email_direct: "Ontvang een mail wanneer iemand je citeert, reageert op je bericht of je @gebruikersnaam noemt." email_private_messages: Ontvang een mail wanneer iemand je een privé-bericht heeft gestuurd. + email_always: "Ontvang notificaties en topicoverzichten zelfs als ik actief ben op het forum." other_settings: Overige @@ -346,6 +389,7 @@ nl: private_message_info: title: 'Privé-bericht' invite: Nodig anderen uit... + remove_allowed_user: "Weet je zeker dat je {{name}} wil verwijderen uit deze priveconversatie?" email: E-mail username: Gebruikersnaam @@ -378,6 +422,7 @@ nl: authenticating: Authenticatie... awaiting_confirmation: "Je account is nog niet geactiveerd. Gebruik de 'Wachtwoord vergeten'-link om een nieuwe activatie-mail te ontvangen." awaiting_approval: "Je account is nog niet goedgekeurd door iemand van de staf. Je krijgt van ons een mail wanneer dat gebeurd is." + requires_invite: "Toegang tot dit forum is alleen op uitnodiging." not_activated: "Je kan nog niet inloggen. We hebben je een activatie-mail gestuurd (naar {{currentEmail}}). Het kan een aantal minuten duren voor deze aan komt. Check ook je spamfolder." resend_activation_email: Klik hier om de activatiemail opnieuw te ontvangen. sent_activation_email_again: "We hebben een nieuwe activatiemail gestuurd naar {{currentEmail}}. Het kan een aantal minuten duren voor deze aan komt. Check ook je spamfolder." @@ -453,8 +498,8 @@ nl: link_optional_text: optionele titel quote_title: Citaat quote_text: Citaat - code_title: Code voorbeeld - code_text: hier de code + code_title: Opgemaakte tekst + code_text: geef hier de opgemaakte tekst upload_title: Afbeelding upload_description: geef een omschrijving voor de afbeelding op olist_title: Genummerde lijst @@ -485,7 +530,7 @@ nl: private_message: " {{username}} {{link}}" invited_to_private_message: " {{username}} {{link}}" invitee_accepted: " {{username}} heeft je uitnodiging geaccepteerd en heeft zich ingeschreven om deel te nemen." - moved_post: " {{username}} verplaatst naar {{link}}" + moved_post: " {{username}} verplaatst {{link}}" total_flagged: aantal gemarkeerde berichten upload_selector: @@ -497,6 +542,8 @@ nl: remote_tip_with_attachments: "vul een internetadres in van een afbeelding of bestand in deze vorm: http://example.com/bestand.ext (toegestane extensies: {{authorized_extensions}})." local_tip: "klik om een afbeelding vanaf je apparaat te selecteren." local_tip_with_attachments: "klik om een afbeelding of bestand vanaf je apparaat te selecteren (toegestane extensies: {{authorized_extensions}})." + hint: "(je kan afbeeldingen ook slepen in de editor om deze te uploaden)" + hint_for_chrome: "(je kan afbeeldingen ook slepen of plakken in de editor om deze te uploaden)" uploading: Afbeelding uploaden search: @@ -545,11 +592,18 @@ nl: title: Details topic rangorde topic: + filter_to: "Laat alleen de {{post_count}} berichten van {{username}} in deze topic zien" create: Maak topic create_long: Maak een nieuw topic private_message: Stuur een privé-bericht list: Topics new: nieuw topic + new_topics: + one: 1 nieuwe topic + other: "{{count}} nieuwe topics" + unread_topics: + one: 1 ongelezen topic + other: "{{count}} ongelezen topics" title: Topic loading_more: Er worden meer topics geladen... loading: Bezig met laden van topic... @@ -607,7 +661,6 @@ nl: auto_close_notice: "Deze topic wordt automatisch over %{timeLeft} gesloten." auto_close_title: 'Instellingen voor automatisch sluiten' auto_close_save: Opslaan - auto_close_cancel: Annuleren auto_close_remove: Sluit deze topic niet automatisch progress: @@ -713,7 +766,7 @@ nl: split_topic: title: Splits topic action: splits topic - topic_name: "Naam nieuwe topic:" + topic_name: "Naam nieuwe topic" error: "Er ging iets mis bij het splitsen van die topic." instructions: one: "Je staat op het punt een nieuwe topic aan te maken en het te vullen met het bericht dat je geselecteerd hebt." @@ -730,6 +783,7 @@ nl: multi_select: select: selecteer selected: "geselecteerd ({{count}})" + select_replies: "selecteer +antwoorden" delete: verwijder geselecteerde cancel: annuleer selectie description: @@ -782,6 +836,12 @@ nl: undelete: herstel dit bericht share: deel een link naar dit bericht more: Meer + delete_replies: + confirm: + one: "Wil je ook het directe antwoord op dit bericht verwijderen?" + other: "Wil je ook de {{count}} directe antwoorden op dit bericht verwijderen?" + yes_value: "Ja, verwijder deze antwoorden ook" + no_value: "Nee, alleen dit bericht" actions: flag: 'Markeer' @@ -887,6 +947,7 @@ nl: category: can: 'can… ' none: (geen categorie) + choose: 'Selecteer een categorie…' edit: bewerk edit_long: Bewerk categorie view: Bekijk topics in categorie @@ -917,6 +978,9 @@ nl: auto_close_label: "Sluit topics automatisch na:" edit_permissions: Wijzig permissies add_permission: Nieuwe permissie + this_year: dit jaar + position: positie + parent: Bovenliggende categorie flagging: title: Waarom meld je dit bericht? @@ -924,7 +988,7 @@ nl: take_action: Onderneem actie notify_action: Meld delete_spammer: Verwijder spammer - delete_confirm: "Je gaat nu %{posts} berichten en %{topics} van deze gebruiker verwijderen, hun account verwijderen en hun e-mailadres %{email} op een permanente blokkeerlijst zetten. Weet je zeker dat dit een spammer is?" + delete_confirm: "Je gaat nu %{posts} berichten en %{topics} van deze gebruiker verwijderen, hun account verwijderen, nieuwe aanmeldingen vanaf hun IP-adres %{ip_address} blokkeren en hun e-mailadres %{email} op een permanente blokkeerlijst zetten. Weet je zeker dat dit een spammer is?" yes_delete_spammer: "Ja, verwijder spammer" cant: "Sorry, je kan dit bericht momenteel niet melden." custom_placeholder_notify_user: "Wat maakt dat je de schrijver persoonlijk iets wil melden? Wees specifiek, constructief en altijd aardig." @@ -1028,8 +1092,8 @@ nl: critical_available: Er is een belangrijke update beschikbaar updates_available: Er zijn updates beschikbaar please_upgrade: Werk de software bij alsjeblieft - no_check_performed: Er is nog niet op updates gecontroleerd. Zorgen sidekiq loopt." - stale_data: Er is al een tijdje niet op updates gecontroleerd. Zorgen sidekiq loopt." + no_check_performed: Er is nog niet op updates gecontroleerd. Zorgen dat sidekiq loopt." + stale_data: Er is al een tijdje niet op updates gecontroleerd. Zorgen dat sidekiq loopt." installed_version: Geïnstalleerd latest_version: Recent problems_found: 'Er zijn een aantal problemen gevonden met je Discourse installatie:' @@ -1110,12 +1174,18 @@ nl: delete_failed: "Unable to delete group. If this is an automatic group, it cannot be destroyed." api: + generate_master: Genereer Master API Key + none: Er zijn geen actieve API keys + user: Gebruiker title: API - long_title: "API informatie" - key: "Key" + key: API Key generate: "Genereer API Key" regenerate: "Genereer API Key opnieuw" + revoke: Intrekken + confirm_regen: Weet je zeker dat je die API Key wil vervangen door een nieuwe? + confirm_revoke: Weet je zeker dat je die API Key wil intrekken? info_html: "Met deze API key kun je met behulp van JSON calls topics maken en bewerken." + all_users: Alle gebruikers note_html: "Houd deze key geheim, gebruikers die deze key hebben kunnen zich als elke andere gebruiker voordoen op het forum en topics aanmaken." customize: @@ -1123,6 +1193,8 @@ nl: long_title: Aanpassingen aan de site header: Header css: Stylesheet + mobile_header: Mobiele header + mobile_css: Mobiele stylesheet override_default: Sluit de standaard stylesheet uit enabled: Ingeschakeld? preview: voorbeeld @@ -1174,12 +1246,44 @@ nl: clear_filters: Bekijk alles staff_user: Staflid target_user: Selecteer gebruiker + subject: Onderwerp when: Wanneer context: Context details: Details + previous_value: Vorige + new_value: Nieuw + diff: Diff + show: Bekijk + modal_title: Details + no_previous: Er is geen vorige waarde + deleted: Geen nieuwe waarde. De record was verwijderd. actions: delete_user: verwijder gebruiker change_trust_level: verander trust level + change_site_setting: verander instellingen + change_site_customization: verander site aanpassingen + delete_site_customization: verwijder site aanpassingen + ban_user: ban gebruiker + unban_user: hef ban op + screened_emails: + title: Gescreende e-mails + description: Nieuwe accounts met een van deze mailadressen worden geblokkeerd of een andere actie wordt ondernomen. + email: E-mailadres + screened_urls: + title: Gescreende urls + description: Deze urls zijn gebruikt door gebruikers die als spammer gemarkeerd zijn. + url: URL + screened_ips: + title: Gescreende ip-adressen + description: IP-adressen die in de gaten worden gehouden. Kies 'sta toe' om deze op een witte lijst te zetten. + delete_confirm: "Weet je zeker dat je de regel voor %{ip_address} wil verwijderen?" + actions: + block: Blokkeer + do_nothing: Sta toe + form: + label: "Nieuw:" + ip_address: IP-adres + add: Voeg toe impersonate: title: Log in als gebruiker @@ -1206,6 +1310,9 @@ nl: approved_selected: one: accepteer lid other: "accepteer {{count}} leden" + reject_selected: + one: weiger lid + other: "weiger {{count}} leden" titles: active: 'Actieve leden' new: 'Nieuwe leden' @@ -1219,12 +1326,23 @@ nl: moderators: Moderators blocked: Geblokkeerde leden banned: Verbannen leden + reject_successful: + one: "1 Gebruiker met succes geweigerd" + other: "%{count} Gebruikers met succes geweigerd" + reject_failures: + one: "Weigering van 1 gebruiker is niet gelukt" + other: "Weigering van %{count} gebruikers is niet gelukt" user: ban_failed: "Er ging iets fout met het blokkeren van deze gebruiker: {{error}}" unban_failed: "Er ging iets fout bij het deblokkeren van deze gebruiker: {{error}}" - ban_duration: "Hoe lang wil je deze gebruiker blokkeren? (dagen)" + ban_duration: "Hoe lang wil je deze gebruiker blokkeren?" + ban_duretion_units: (dagen) + ban_reason_label: "Waarom ban je? Als de gebruiker in probeert te loggen, zullen ze deze tekst zien. Hou het kort." + ban_reason: Reden voor ban + banned_by: Verbannen door delete_all_posts: Verwijder alle berichten + delete_all_posts_confirm: "Je gaat %{posts} en %{topics} verwijderen. Zeker weten?" ban: Blokkeer unban: Deblokkeer banned: Geblokkeerd? @@ -1263,8 +1381,8 @@ nl: one: "Gebruikers kunnen niet worden verwijders als ze zich meer dan %{count} dag geleden registreerden of als ze berichten geplaatst hebben. Verwijder alle berichten voordat je een gebruiker probeert te verwijderen." other: "Gebruikers kunnen niet worden verwijders als ze zich meer dan %{count} dagen geleden registreerden of als ze berichten geplaatst hebben. Verwijder alle berichten voordat je een gebruiker probeert te verwijderen." delete_confirm: Weet je zeker dat je deze gebruiker definitief wil verwijderen? Deze handeling is permanant! - delete_and_block: "Ja, en blokkeer registraties met datzelfde e-mailadres" - delete_dont_block: "Ja, maar sta nieuwe registraties toe met datzelfde e-mailadres" + delete_and_block: "Ja, en blokkeer registraties met datzelfde e-mail- en IP-adres" + delete_dont_block: "Ja, maar sta nieuwe registraties toe met datzelfde e-mail- en IP-adres" deleted: De gebruiker is verwijderd. delete_failed: Er ging iets mis bij het verwijderen van deze gebruiker. Zorg er voor dat alle berichten van deze gebruiker eerst verwijderd zijn. send_activation_email: Verstuur activatiemail @@ -1280,6 +1398,7 @@ nl: banned_explanation: Een verbannen gebruiker kan niet meer inloggen. block_explanation: Een geblokkeerde gebruiker kan geen topics maken of reageren op topics. trust_level_change_failed: Er ging iets mis bij het wijzigen van het trust level van deze gebruiker. + ban_modal_title: Ban gebruiker site_content: none: Selecteer een tekst om deze te bewerken From cc27c5f363e1daf9384f9ab6bb0db73bf8fd71ef Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 4 Nov 2013 09:58:34 +1100 Subject: [PATCH 14/25] Rails 4 changes behavior of precompile not to include originals This patch will restore rails 3 beavior and fix emoji etc under rails 4 --- lib/tasks/assets.rake | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 lib/tasks/assets.rake diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake new file mode 100644 index 0000000000000..ad1c6b28c9915 --- /dev/null +++ b/lib/tasks/assets.rake @@ -0,0 +1,20 @@ +task 'assets:precompile' => 'environment' do + # see: https://github.com/rails/sprockets-rails/issues/49 + # a decision was made no longer to copy non-digested assets + # this breaks stuff like the emoji plugin. We could fix it, + # but its a major pain with little benefit. + if rails4? + puts "Copying non-digested versions of assets" + assets = Dir.glob(File.join(Rails.root, 'public/assets/**/*')) + regex = /(-{1}[a-z0-9]{32}*\.{1}){1}/ + assets.each do |file| + next if File.directory?(file) || file !~ regex + + source = file.split('/') + source.push(source.pop.gsub(regex, '.')) + + non_digested = File.join(source) + FileUtils.cp(file, non_digested) + end + end +end From 934ede7d912a0a32783bd226263bd6a2d2b57764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 4 Nov 2013 11:52:12 +0100 Subject: [PATCH 15/25] shorter message for untranslated strings --- app/assets/javascripts/discourse.js | 1 - app/assets/javascripts/env.js | 2 -- app/assets/javascripts/locales/i18n.js | 33 +++++-------------- .../common/_discourse_javascript.html.erb | 15 +++------ app/views/layouts/application.html.erb | 1 + 5 files changed, 14 insertions(+), 38 deletions(-) diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 094c4e9bfc1b7..36d5edfa2ed88 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -248,4 +248,3 @@ Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { }); Discourse.Router = Discourse.Router.reopen({ location: 'discourse_location' }); - diff --git a/app/assets/javascripts/env.js b/app/assets/javascripts/env.js index 98996e8c05acb..a8781abcb8e7a 100644 --- a/app/assets/javascripts/env.js +++ b/app/assets/javascripts/env.js @@ -8,5 +8,3 @@ window.ENV = { window.Discourse = {}; Discourse.SiteSettings = {}; - - diff --git a/app/assets/javascripts/locales/i18n.js b/app/assets/javascripts/locales/i18n.js index 3e187afae731f..d8591f32715f0 100644 --- a/app/assets/javascripts/locales/i18n.js +++ b/app/assets/javascripts/locales/i18n.js @@ -60,8 +60,7 @@ I18n.locale = null; // Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`. I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm; -I18n.fallbackRules = { -}; +I18n.fallbackRules = {}; I18n.pluralizationRules = { en: function (n) { @@ -207,7 +206,7 @@ I18n.translate = function(scope, options) { } else { return this.interpolate(translation, options); } - } catch(err) { + } catch (error) { return this.missingTranslation(scope); } }; @@ -485,15 +484,9 @@ I18n.findAndTranslateValidNode = function(keys, translation) { I18n.pluralize = function(count, scope, options) { var translation; - try { - translation = this.lookup(scope, options); - } catch (error) {} + try { translation = this.lookup(scope, options); } catch (error) {} + if (!translation) { return this.missingTranslation(scope); } - if (!translation) { - return this.missingTranslation(scope); - } - - var message; options = this.prepareOptions(options); options.count = count.toString(); @@ -501,24 +494,16 @@ I18n.pluralize = function(count, scope, options) { var key = pluralizer(Math.abs(count)); var keys = ((typeof key == "object") && (key instanceof Array)) ? key : [key]; - message = this.findAndTranslateValidNode(keys, translation); + var message = this.findAndTranslateValidNode(keys, translation); if (message == null) message = this.missingTranslation(scope, keys[0]); return this.interpolate(message, options); }; -I18n.missingTranslation = function() { - var message = '[missing "' + this.currentLocale() - , count = arguments.length - ; - - for (var i = 0; i < count; i++) { - message += "." + arguments[i]; - } - - message += '" translation]'; - - return message; +I18n.missingTranslation = function(scope, key) { + var message = '[' + this.currentLocale() + "." + scope; + if (key) { message += "." + key; } + return message + ']'; }; I18n.currentLocale = function() { diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index 151927422e7a4..63ca917259aaf 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -7,23 +7,16 @@ 'defer/google_diff_match_patch': <%= asset_path('defer/google_diff_match_patch.js').inspect.html_safe %> }; - var assetPath = function(asset){ - return map[asset]; - }; - - return assetPath; + return function(asset){ return map[asset]; }; })(); - diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 473ac8ab12977..0ef90532ce4ef 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,6 +11,7 @@ + <%= javascript_include_tag "preload_store" %> <%= javascript_include_tag "locales/#{I18n.locale}" %> From 12ac2f2e99968d6b3d7f745a70fb3204a0d64d39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 4 Nov 2013 12:51:39 +0100 Subject: [PATCH 16/25] FIX issues with quote button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix the following issues - Quote button quotes only one word on iOS - Quote reply popup should not come up when selecting the text in the ‘reply’ button --- .../controllers/quote_button_controller.js | 4 +- .../javascripts/discourse/lib/utilities.js | 20 +++---- .../discourse/views/quote_button_view.js | 4 +- .../stylesheets/common/foundation/mixins.scss | 56 +------------------ app/assets/stylesheets/desktop/compose.scss | 10 ++-- app/assets/stylesheets/desktop/topic.scss | 5 +- app/assets/stylesheets/desktop/user.scss | 4 +- app/assets/stylesheets/mobile/compose.scss | 8 +-- .../stylesheets/mobile/magnific-popup.scss | 12 +--- app/assets/stylesheets/mobile/user.scss | 4 +- 10 files changed, 30 insertions(+), 97 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/quote_button_controller.js b/app/assets/javascripts/discourse/controllers/quote_button_controller.js index 8c1b9110ac801..c6e4ceb7e5874 100644 --- a/app/assets/javascripts/discourse/controllers/quote_button_controller.js +++ b/app/assets/javascripts/discourse/controllers/quote_button_controller.js @@ -45,9 +45,7 @@ Discourse.QuoteButtonController = Discourse.Controller.extend({ cloned = range.cloneRange(), $ancestor = $(range.commonAncestorContainer); - // don't display the "quote reply" button if you select text spanning two posts - // note: the ".contents" is here to prevent selection of the topic summary - if ($ancestor.closest('.topic-body > .contents').length === 0) { + if ($ancestor.closest('.cooked').length === 0) { this.set('buffer', ''); return; } diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index e2e3060945b78..45a712d1387cd 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -88,18 +88,18 @@ Discourse.Utilities = { var html = ''; if (typeof window.getSelection !== "undefined") { - var sel = window.getSelection(); - if (sel.rangeCount) { - var container = document.createElement("div"); - for (var i = 0, len = sel.rangeCount; i < len; ++i) { - container.appendChild(sel.getRangeAt(i).cloneContents()); - } - html = container.innerHTML; + var sel = window.getSelection(); + if (sel.rangeCount) { + var container = document.createElement("div"); + for (var i = 0, len = sel.rangeCount; i < len; ++i) { + container.appendChild(sel.getRangeAt(i).cloneContents()); } + html = container.innerHTML; + } } else if (typeof document.selection !== "undefined") { - if (document.selection.type === "Text") { - html = document.selection.createRange().htmlText; - } + if (document.selection.type === "Text") { + html = document.selection.createRange().htmlText; + } } // Strip out any .click elements from the HTML before converting it to text diff --git a/app/assets/javascripts/discourse/views/quote_button_view.js b/app/assets/javascripts/discourse/views/quote_button_view.js index 9c6d02949dc53..7741ac08725b2 100644 --- a/app/assets/javascripts/discourse/views/quote_button_view.js +++ b/app/assets/javascripts/discourse/views/quote_button_view.js @@ -64,8 +64,8 @@ Discourse.QuoteButtonView = Discourse.View.extend({ }) .on('selectionchange', function() { // there is no need to handle this event when the mouse is down - // or if there is not a touch in progress - if (view.get('isMouseDown') || !view.get('isTouchInProgress')) return; + // or if there a touch in progress + if (view.get('isMouseDown') || view.get('isTouchInProgress')) return; // `selection.anchorNode` is used as a target view.selectText(window.getSelection().anchorNode, controller); }); diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index dbc8b5c076f82..830072b3a4b9d 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -55,23 +55,13 @@ // Border radius @mixin border-radius-all($radius) { - border-radius: $radius; -} - -@mixin border-radius-top($radius) { - border-top-right-radius: $radius; - border-top-left-radius: $radius; -} - -@mixin border-radius-bottom($radius) { - border-bottom-right-radius: $radius; - border-bottom-left-radius: $radius; + border-radius: $radius; } // Box shadow @mixin box-shadow($shadow) { - box-shadow: $shadow; + box-shadow: $shadow; } // Linear gradient @@ -81,32 +71,6 @@ background-image: linear-gradient(to bottom, $start-color, $end-color); } -// Background size - -@mixin background-size($size) { - background-size: $size; -} - -// Background clip - -@mixin background-clip($clip) { - background-clip: $clip; -} - -// Rotate - -@mixin rotate($degrees) { - -webkit-transform: rotate($degrees); - transform: rotate($degrees); -} - -// Scale - -@mixin scale($ratio) { - -webkit-transform: scale($ratio); - transform: scale($ratio); -} - // Transition @mixin transition($transition) { @@ -138,12 +102,6 @@ } } -@mixin fade-soft($time: 1s) { - -webkit-transition: opacity $time ease-in-out; - -ms-transition: opacity $time ease-in-out; - transition: opacity $time ease-in-out; -} - @mixin visible { opacity: 1; visibility: visible; @@ -151,16 +109,6 @@ transition-delay: 0s; } -// Decorations -// -------------------------------------------------- - -// Glow - -@mixin glow($color) { - border: 1px solid $color; - box-shadow: 0 0 5px $color; -} - // // -------------------------------------------------- diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index bbfb05443aa79..05592c8ca56e4 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -309,7 +309,7 @@ border: 1px dashed $gray; overflow: auto; visibility: visible; - + &.hidden { width: 0; visibility: hidden; @@ -432,8 +432,7 @@ div.ac-wrap { } #wmd-input, #wmd-preview { - box-sizing: border-box; - -moz-box-sizing: border-box; + @include box-sizing(border-box); width: 100%; height: 100%; min-height: 100%; @@ -453,7 +452,7 @@ div.ac-wrap { top: 0; height: 100%; min-height: 100%; - box-sizing: border-box; + @include box-sizing(border-box); border: 0; border-top: 36px solid transparent; @include border-radius-all(0); @@ -461,8 +460,7 @@ div.ac-wrap { } .textarea-wrapper, .preview-wrapper { position: relative; - box-sizing: border-box; - -moz-box-sizing: border-box; + @include box-sizing(border-box); height: 100%; min-height: 100%; margin: 0; diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index bbec999656b73..cdaf714e1a7d7 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -11,13 +11,14 @@ } .post-actions { + @include unselectable; clear: both; text-align: right; - .post-action { + .post-action { display: inline-block; margin-left: 10px; margin-top: 10px; - } + } } .post-menu-area { margin-bottom: 10px; diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 505f543edc6dc..fef462b893ad1 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -78,9 +78,7 @@ .btn { width: 100%; margin-bottom: 5px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + @include box-sizing(border-box); } } h2 { diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index f57a0ac0c3e48..b8d2fb34af6ff 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -389,8 +389,7 @@ div.ac-wrap { } #wmd-input, #wmd-preview { - box-sizing: border-box; - -moz-box-sizing: border-box; + @include box-sizing(border-box); width: 100%; height: 100%; min-height: 100%; @@ -410,7 +409,7 @@ div.ac-wrap { top: 0; height: 100%; min-height: 100%; - box-sizing: border-box; + @include box-sizing(border-box); border: 0; border-top: 36px solid transparent; @include border-radius-all(0); @@ -418,8 +417,7 @@ div.ac-wrap { } .textarea-wrapper, .preview-wrapper { position: relative; - box-sizing: border-box; - -moz-box-sizing: border-box; + @include box-sizing(border-box); height: 100%; min-height: 100%; margin: 0; diff --git a/app/assets/stylesheets/mobile/magnific-popup.scss b/app/assets/stylesheets/mobile/magnific-popup.scss index 872ae8f2b7b7a..1c6cdb13a9af8 100644 --- a/app/assets/stylesheets/mobile/magnific-popup.scss +++ b/app/assets/stylesheets/mobile/magnific-popup.scss @@ -147,9 +147,7 @@ $caption-subtitle-color: #BDBDBD !default; left: 0; top: 0; padding: 0 $popup-padding-left; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + @include box-sizing(border-box); } // Vertical centerer helper @@ -453,9 +451,7 @@ button::-moz-focus-inner { height: auto; display: block; line-height: 0; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + @include box-sizing(border-box); padding: $image-padding-top 0 $image-padding-bottom; margin: 0 auto; } @@ -534,9 +530,7 @@ button::-moz-focus-inner { top: auto; padding: 3px 5px; position: fixed; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + @include box-sizing(border-box); } .mfp-img-mobile .mfp-bottom-bar:empty { padding: 0; diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 4ac9aeb6429ab..bfdd68e26898f 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -73,9 +73,7 @@ .btn { width: 100%; margin-bottom: 5px; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; + @include box-sizing(border-box); } } h2 { From 4f8d0a1a963df6a18eb53cd32d2c54b448d4531f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 4 Nov 2013 14:44:52 +0100 Subject: [PATCH 17/25] revert changes to magnific-popup.scss --- app/assets/stylesheets/mobile/magnific-popup.scss | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/mobile/magnific-popup.scss b/app/assets/stylesheets/mobile/magnific-popup.scss index 1c6cdb13a9af8..c2ff7391bb02d 100644 --- a/app/assets/stylesheets/mobile/magnific-popup.scss +++ b/app/assets/stylesheets/mobile/magnific-popup.scss @@ -147,7 +147,9 @@ $caption-subtitle-color: #BDBDBD !default; left: 0; top: 0; padding: 0 $popup-padding-left; - @include box-sizing(border-box); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } // Vertical centerer helper @@ -451,7 +453,9 @@ button::-moz-focus-inner { height: auto; display: block; line-height: 0; - @include box-sizing(border-box); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; padding: $image-padding-top 0 $image-padding-bottom; margin: 0 auto; } @@ -530,7 +534,9 @@ button::-moz-focus-inner { top: auto; padding: 3px 5px; position: fixed; - @include box-sizing(border-box); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; } .mfp-img-mobile .mfp-bottom-bar:empty { padding: 0; From 4ce711a3b78472152ca54fd36de9b1ce4bd56300 Mon Sep 17 00:00:00 2001 From: Kris Aubuchon Date: Mon, 4 Nov 2013 09:16:47 -0500 Subject: [PATCH 18/25] some general simplification to modal, menus, etc --- app/assets/stylesheets/desktop/modal.scss | 7 +------ app/assets/stylesheets/desktop/share_link.scss | 7 +++---- app/assets/stylesheets/desktop/topic-post.scss | 8 ++------ app/assets/stylesheets/desktop/topic.scss | 5 +---- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index 4d124dce19ee2..b56637e2d2184 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -56,7 +56,6 @@ *border: 1px solid #999; /* IE6-7 */ - @include border-radius-all (6px); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); background-clip: padding-box; } @@ -85,7 +84,6 @@ *border: 1px solid #999; /* IE6-7 */ - @include border-radius-all (6px); box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); background-clip: padding-box; } @@ -110,8 +108,6 @@ margin: 0 15px; padding: 14px 0 15px; border-top: 1px solid #ddd; - @include border-radius-all(0 0 6px 6px); - @include box-shadow (inset 0 1px 0 #ffffff); *zoom: 1; } .modal-footer:before, @@ -134,8 +130,7 @@ margin-left: -1px; } .modal-header { - border-bottom: 1px solid #9baab2; - @include box-shadow((0 1px 3px rgba($black, 0.12), inset 0 -4px 4px -4px rgba($black, 0.3))); + border-bottom: 1px solid #ddd; h3 { color: $nav-pills-background-color-active; font-size: 20px; diff --git a/app/assets/stylesheets/desktop/share_link.scss b/app/assets/stylesheets/desktop/share_link.scss index 3ffdc039a3031..f26a066e59152 100644 --- a/app/assets/stylesheets/desktop/share_link.scss +++ b/app/assets/stylesheets/desktop/share_link.scss @@ -7,10 +7,9 @@ position: absolute; left: 20px; z-index: 990; - @include border-radius-all(3px); - @include box-shadow(1px 1px 5px $darkish_gray); - background-color: $light_gray; - padding: 3px 7px 6px 7px; + box-shadow: 0 1px 5px rgba(70, 70, 70, .4); + background-color: #fff; + padding: 6px 10px 10px 10px; width: 300px; display: none; &.visible { diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 374f2db569171..34a5b0b4349ff 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -772,12 +772,8 @@ button.show-replies { margin: 1px 0 0; list-style: none; background-color: #ffffff; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.2); - *border-right-width: 2px; - *border-bottom-width: 2px; - border-radius: 5px; - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + border: 1px solid #ddd; + box-shadow: 0 1px 5px rgba(70, 70, 70, .4); background-clip: padding-box; span {font-size: 12px;} .title {font-weight: bold; display: block; font-size: 14px;} diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index bbec999656b73..83d1f248e6030 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -45,9 +45,7 @@ margin-left: 10px; } -.docked #topic-progress { - box-shadow: 0 0 3px #aaa; -} + #topic-progress-wrapper { position: fixed; @@ -81,7 +79,6 @@ a:hover.reply-new { } #topic-progress { - box-shadow: 0 0 3px #000; position: relative; left: 302px; &.hidden { From 5e69b277ead5c1e684927856cdbf884568e6b342 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Nov 2013 11:47:23 -0500 Subject: [PATCH 19/25] FIX: emoticon :poop: wasn't working :) --- .../vendor/assets/javascripts/discourse_emoji.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js index 394138c5cfdf2..c2a2cfac7ce62 100644 --- a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js +++ b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js @@ -37,20 +37,19 @@ ":-$" : 'blush' }; - Object.keys(translations).forEach(function (code) { + emoji.forEach(function (e) { + Discourse.Dialect.inlineReplace(":" + e + ":", function(code) { + return imageFor(e); + }); + }); + Object.keys(translations).forEach(function (code) { var replacement = translations[code]; Discourse.Dialect.inlineReplace(code, function (code) { return imageFor(replacement); }); }); - Discourse.Dialect.inlineBetween({ - between: ':', - rawContents: true, - emitter: imageFor - }); - if (Discourse && Discourse.ComposerView) { Discourse.ComposerView.on("initWmdEditor", function(event){ From 1c2b5015a3dd53c17e932030b67a4836042a6004 Mon Sep 17 00:00:00 2001 From: Kris Aubuchon Date: Mon, 4 Nov 2013 12:45:18 -0500 Subject: [PATCH 20/25] fixing user title line-height, adding @mention style back in, consistent category width/alignment --- .../stylesheets/desktop/topic-list.scss | 29 ++++++++++++------- .../stylesheets/desktop/topic-post.scss | 15 +++++++--- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index e460bf46230dd..c8d7b5897c462 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -21,10 +21,13 @@ font-weight: normal; } - a.badge-category {padding: 3px 12px; font-size: 16px; + a.badge-category {padding: 3px 12px; font-size: 16px; + + &.category-dropdown-button { + padding: 3px 9px 2px 9px; + + i {height: 20px;} - &.category-dropdown-button { padding: 3px 10px 3px 8px; - font-size: 14px; } } @@ -34,7 +37,6 @@ font-weight: normal; text-transform: capitalize; - &.home {padding: 3px 12px;} } @@ -85,7 +87,7 @@ } th, td { - padding: 7px 5px; + padding: 9px 5px; line-height: 1.25; text-align: left; vertical-align: middle; @@ -116,7 +118,7 @@ position: relative; } + .main-link { - padding-left: 0; + padding-left: 5px; } } .main-link { @@ -451,20 +453,27 @@ ol.category-breadcrumb { position: absolute; border: 1px solid #ccc; background-color: white; - max-width: 300px; - min-width: 140px; height: 200px; - padding: 8px 5px 0 7px; + padding: 8px 7px 0 7px; z-index: 100; margin-top: 30px; + min-width: 116px; + a.badge-category { font-size: 13px; font-weight: bold; - padding: 4px 6px; + padding: 4px 0; float: none; line-height: 19px; text-transform: none; + width: 100%; + min-width: 102px; + + text-align: center; + margin-right: 20px; + + } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index ca894819b0200..e7a043ca1adc7 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -501,8 +501,17 @@ iframe { h1, h2, h3 {margin: 10px 0;} ul, ol {margin: 0 15px;} li p {margin: 3px 0;} + + } + a.mention { + padding: 2px 4px; + color: #666; + background: #eee; + } + + .modal-body { input[type=text] { font-size: 16px; @@ -587,10 +596,11 @@ position: relative; } .user-title { - margin-top: 5px; + margin-top: 8px; color: #aaa; overflow: hidden; font-size: 80%; + line-height: 13px; } .info-line { @@ -599,9 +609,6 @@ position: relative; } - - - blockquote { /* solo quotes */ margin: 14px 0; padding: 12px; From ede59a4386e24ad1bb7ecbbec176f60ef5e0cf75 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 4 Nov 2013 12:51:01 -0500 Subject: [PATCH 21/25] FIX: issue 1538. After upgrading and before a new version check request has been made, dashboard might still say that an update is available. --- .../javascripts/admin/models/version_check.js | 2 +- .../admin/templates/dashboard.js.handlebars | 16 +- .../stylesheets/common/admin/admin_base.scss | 2 +- app/jobs/scheduled/version_check.rb | 1 + app/models/discourse_version_check.rb | 2 +- config/locales/client.en.yml | 1 + lib/discourse_updates.rb | 27 +++- spec/components/discourse_updates_spec.rb | 140 +++++++++++------- .../admin/versions_controller_spec.rb | 1 + test/javascripts/models/version_check_test.js | 2 + 10 files changed, 126 insertions(+), 68 deletions(-) diff --git a/app/assets/javascripts/admin/models/version_check.js b/app/assets/javascripts/admin/models/version_check.js index 2b05fb87b3c62..2add3b7b2f0cb 100644 --- a/app/assets/javascripts/admin/models/version_check.js +++ b/app/assets/javascripts/admin/models/version_check.js @@ -13,7 +13,7 @@ Discourse.VersionCheck = Discourse.Model.extend({ }.property('updated_at'), dataIsOld: function() { - return moment().diff(moment(this.get('updated_at')), 'hours') >= 48; + return this.get('version_check_pending') || moment().diff(moment(this.get('updated_at')), 'hours') >= 48; }.property('updated_at'), staleData: function() { diff --git a/app/assets/javascripts/admin/templates/dashboard.js.handlebars b/app/assets/javascripts/admin/templates/dashboard.js.handlebars index 621d355ee61f6..fdc9fdd5f16db 100644 --- a/app/assets/javascripts/admin/templates/dashboard.js.handlebars +++ b/app/assets/javascripts/admin/templates/dashboard.js.handlebars @@ -62,16 +62,26 @@ {{#if versionCheck.staleData}}   - + {{#if versionCheck.version_check_pending}} + + {{else}} + + {{/if}} - {{i18n admin.dashboard.stale_data}} + + {{#if versionCheck.version_check_pending}} + {{i18n admin.dashboard.version_check_pending}} + {{else}} + {{i18n admin.dashboard.stale_data}} + {{/if}} + {{else}} {{ versionCheck.latest_version }} {{#if versionCheck.upToDate }} - + {{else}} {{#if versionCheck.behindByOneVersion}} diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 22696753c0d5a..12fed2b023127 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -382,7 +382,7 @@ table { font-size: 26px; } - .update-to-date { + .up-to-date { color: green; } .updates-available { diff --git a/app/jobs/scheduled/version_check.rb b/app/jobs/scheduled/version_check.rb index 91528bf7c544d..a50e7c2fef396 100644 --- a/app/jobs/scheduled/version_check.rb +++ b/app/jobs/scheduled/version_check.rb @@ -11,6 +11,7 @@ def execute(args) should_send_email = (SiteSetting.new_version_emails and DiscourseUpdates.missing_versions_count and DiscourseUpdates.missing_versions_count == 0) json = DiscourseHub.discourse_version_check + DiscourseUpdates.last_installed_version = Discourse::VERSION::STRING DiscourseUpdates.latest_version = json['latestVersion'] DiscourseUpdates.critical_updates_available = json['criticalUpdates'] DiscourseUpdates.missing_versions_count = json['missingVersionsCount'] diff --git a/app/models/discourse_version_check.rb b/app/models/discourse_version_check.rb index 00e21d04f0214..01a1a9d0524e0 100644 --- a/app/models/discourse_version_check.rb +++ b/app/models/discourse_version_check.rb @@ -7,7 +7,7 @@ class DiscourseVersionCheck include ActiveModel::Serialization end - attr_accessor :latest_version, :critical_updates, :installed_version, :installed_sha, :missing_versions_count, :updated_at + attr_accessor :latest_version, :critical_updates, :installed_version, :installed_sha, :missing_versions_count, :updated_at, :version_check_pending unless rails4? def active_model_serializer diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f51176145b03f..619cf683100af 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1090,6 +1090,7 @@ en: please_upgrade: "Please upgrade!" no_check_performed: "A check for updates has not been performed. Ensure sidekiq is running." stale_data: "A check for updates has not been performed lately. Ensure sidekiq is running." + version_check_pending: "Looks like you upgraded recently. Fantastic!" installed_version: "Installed" latest_version: "Latest" problems_found: "Some problems have been found with your installation of Discourse:" diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb index 918c6d1c84b1d..250e2f8975d01 100644 --- a/lib/discourse_updates.rb +++ b/lib/discourse_updates.rb @@ -24,20 +24,27 @@ def check_version # Handle cases when version check data is old so we report something that makes sense - if (version_info.updated_at.nil? or - (version_info.missing_versions_count == 0 and version_info.latest_version != version_info.installed_version) or - (version_info.missing_versions_count != 0 and version_info.latest_version == version_info.installed_version)) + if (version_info.updated_at.nil? or # never performed a version check + last_installed_version != Discourse::VERSION::STRING or # upgraded since the last version check + (version_info.missing_versions_count == 0 and version_info.latest_version != version_info.installed_version) or # old data + (version_info.missing_versions_count != 0 and version_info.latest_version == version_info.installed_version)) # old data Jobs.enqueue(:version_check, all_sites: true) - end - - if !version_info.updated_at.nil? and version_info.latest_version == version_info.installed_version - version_info.missing_versions_count = 0 + version_info.version_check_pending = true + unless version_info.updated_at.nil? + version_info.missing_versions_count = 0 + version_info.critical_updates = false + end end end version_info end + # last_installed_version is the installed version at the time of the last version check + def last_installed_version + $redis.get last_installed_version_key + end + def latest_version $redis.get latest_version_key end @@ -59,7 +66,7 @@ def updated_at=(time_with_zone) $redis.set updated_at_key, time_with_zone.as_json end - ['latest_version', 'missing_versions_count', 'critical_updates_available'].each do |name| + ['last_installed_version', 'latest_version', 'missing_versions_count', 'critical_updates_available'].each do |name| eval "define_method :#{name}= do |arg| $redis.set #{name}_key, arg end" @@ -68,6 +75,10 @@ def updated_at=(time_with_zone) private + def last_installed_version_key + 'last_installed_version' + end + def latest_version_key 'discourse_latest_version' end diff --git a/spec/components/discourse_updates_spec.rb b/spec/components/discourse_updates_spec.rb index 7e4e96ce8de53..f3d36e153e4fe 100644 --- a/spec/components/discourse_updates_spec.rb +++ b/spec/components/discourse_updates_spec.rb @@ -16,75 +16,103 @@ def stub_data(latest, missing, critical, updated_at) subject { DiscourseUpdates.check_version.as_json } - context 'a good version check request happened recently' do - context 'and server is up-to-date' do - before { stub_data(Discourse::VERSION::STRING, 0, false, 12.hours.ago) } + context 'version check was done at the current installed version' do + before do + DiscourseUpdates.stubs(:last_installed_version).returns(Discourse::VERSION::STRING) + end - it 'returns all the version fields' do - subject['latest_version'].should == Discourse::VERSION::STRING - subject['missing_versions_count'].should == 0 - subject['critical_updates'].should == false - subject['installed_version'].should == Discourse::VERSION::STRING + context 'a good version check request happened recently' do + context 'and server is up-to-date' do + before { stub_data(Discourse::VERSION::STRING, 0, false, 12.hours.ago) } + + it 'returns all the version fields' do + subject['latest_version'].should == Discourse::VERSION::STRING + subject['missing_versions_count'].should == 0 + subject['critical_updates'].should == false + subject['installed_version'].should == Discourse::VERSION::STRING + end + + it 'returns the timestamp of the last version check' do + subject['updated_at'].should be_within_one_second_of(12.hours.ago) + end end - it 'returns the timestamp of the last version check' do - subject['updated_at'].should be_within_one_second_of(12.hours.ago) + context 'and server is not up-to-date' do + before { stub_data('0.9.0', 2, false, 12.hours.ago) } + + it 'returns all the version fields' do + subject['latest_version'].should == '0.9.0' + subject['missing_versions_count'].should == 2 + subject['critical_updates'].should == false + subject['installed_version'].should == Discourse::VERSION::STRING + end + + it 'returns the timestamp of the last version check' do + subject['updated_at'].should be_within_one_second_of(12.hours.ago) + end end end - context 'and server is not up-to-date' do - before { stub_data('0.9.0', 2, false, 12.hours.ago) } + context 'a version check has never been performed' do + before { stub_data(nil, nil, false, nil) } - it 'returns all the version fields' do - subject['latest_version'].should == '0.9.0' - subject['missing_versions_count'].should == 2 - subject['critical_updates'].should == false + it 'returns the installed version' do subject['installed_version'].should == Discourse::VERSION::STRING end - it 'returns the timestamp of the last version check' do - subject['updated_at'].should be_within_one_second_of(12.hours.ago) + it 'indicates that version check has not been performed' do + subject.should have_key('updated_at') + subject['updated_at'].should == nil end - end - end - context 'a version check has never been performed' do - before { stub_data(nil, nil, false, nil) } + it 'does not return latest version info' do + subject.should_not have_key('latest_version') + subject.should_not have_key('missing_versions_count') + subject.should_not have_key('critical_updates') + end - it 'returns the installed version' do - subject['installed_version'].should == Discourse::VERSION::STRING + it 'queues a version check' do + Jobs.expects(:enqueue).with(:version_check, anything) + subject + end end - it 'indicates that version check has not been performed' do - subject.should have_key('updated_at') - subject['updated_at'].should == nil - end + # These cases should never happen anymore, but keep the specs to be sure + # they're handled in a sane way. + context 'old version check data' do + shared_examples "queue version check and report that version is ok" do + it 'queues a version check' do + Jobs.expects(:enqueue).with(:version_check, anything) + subject + end + + it 'reports 0 missing versions' do + subject['missing_versions_count'].should == 0 + end + + it 'reports that a version check will be run soon' do + subject['version_check_pending'].should == true + end + end - it 'does not return latest version info' do - subject.should_not have_key('latest_version') - subject.should_not have_key('missing_versions_count') - subject.should_not have_key('critical_updates') - end + context 'installed is latest' do + before { stub_data(Discourse::VERSION::STRING, 1, false, 8.hours.ago) } + include_examples "queue version check and report that version is ok" + end - it 'queues a version check' do - Jobs.expects(:enqueue).with(:version_check, anything) - subject + context 'installed does not match latest version, but missing_versions_count is 0' do + before { stub_data('0.10.10.123', 0, false, 8.hours.ago) } + include_examples "queue version check and report that version is ok" + end end end - context 'installed version is newer' do - before { stub_data('0.9.3', 0, false, 28.hours.ago) } - - it 'queues a version check' do - Jobs.expects(:enqueue).with(:version_check, anything) - subject + context 'version check was done at a different installed version' do + before do + DiscourseUpdates.stubs(:last_installed_version).returns('0.9.1') end - end - - context 'old version check data' do - context 'installed is latest' do - before { stub_data(Discourse::VERSION::STRING, 1, false, 8.hours.ago) } + shared_examples "when last_installed_version is old" do it 'queues a version check' do Jobs.expects(:enqueue).with(:version_check, anything) subject @@ -93,16 +121,20 @@ def stub_data(latest, missing, critical, updated_at) it 'reports 0 missing versions' do subject['missing_versions_count'].should == 0 end + + it 'reports that a version check will be run soon' do + subject['version_check_pending'].should == true + end end - context 'installed is not latest' do - before { stub_data('0.9.1', 0, false, 8.hours.ago) } + context 'missing_versions_count is 0' do + before { stub_data('0.9.7', 0, false, 8.hours.ago) } + include_examples "when last_installed_version is old" + end - it 'queues a version check' do - Jobs.expects(:enqueue).with(:version_check, anything) - subject - end + context 'missing_versions_count is not 0' do + before { stub_data('0.9.7', 1, false, 8.hours.ago) } + include_examples "when last_installed_version is old" end end - end diff --git a/spec/controllers/admin/versions_controller_spec.rb b/spec/controllers/admin/versions_controller_spec.rb index 13e0cad7b06a1..364c5503a0b7b 100644 --- a/spec/controllers/admin/versions_controller_spec.rb +++ b/spec/controllers/admin/versions_controller_spec.rb @@ -4,6 +4,7 @@ describe Admin::VersionsController do before do + Jobs::VersionCheck.any_instance.stubs(:execute).returns(true) DiscourseUpdates.stubs(:updated_at).returns(2.hours.ago) DiscourseUpdates.stubs(:latest_version).returns('1.2.33') DiscourseUpdates.stubs(:critical_updates_available?).returns(false) diff --git a/test/javascripts/models/version_check_test.js b/test/javascripts/models/version_check_test.js index 4b6d2b57eaa04..b03102a53af30 100644 --- a/test/javascripts/models/version_check_test.js +++ b/test/javascripts/models/version_check_test.js @@ -7,6 +7,7 @@ test('dataIsOld', function() { dataIsOld({updated_at: moment().subtract('hours', 2).toJSON()}, false, '2 hours ago'); dataIsOld({updated_at: moment().subtract('hours', 49).toJSON()}, true, '49 hours ago'); + dataIsOld({updated_at: moment().subtract('hours', 2).toJSON(), version_check_pending: true}, true, 'version check pending'); }); test('staleData', function() { @@ -21,4 +22,5 @@ test('staleData', function() { staleData({missing_versions_count: 0, installed_version: '0.9.4', latest_version: '0.9.3', updated_at: updatedAt(2)}, true, 'installed and latest do not match, but missing_versions_count is 0'); staleData({missing_versions_count: 1, installed_version: '0.9.3', latest_version: '0.9.3', updated_at: updatedAt(2)}, true, 'installed and latest match, but missing_versions_count is not 0'); staleData({missing_versions_count: 0, installed_version: '0.9.3', latest_version: '0.9.3', updated_at: updatedAt(50)}, true, 'old version check data'); + staleData({version_check_pending: true, missing_versions_count: 0, installed_version: '0.9.4', latest_version: '0.9.3', updated_at: updatedAt(2)}, true, 'version was upgraded, but no version check has been done since the upgrade'); }); From a13d05660fedc2da09f6f9ea21e75a8a3a81cd3e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Nov 2013 13:12:37 -0500 Subject: [PATCH 22/25] FIX: Show status icons on user topic pages --- .../topic_status_component.js} | 14 +++++++------- .../templates/list/basic_topic_list.js.handlebars | 7 ++++--- 2 files changed, 11 insertions(+), 10 deletions(-) rename app/assets/javascripts/discourse/{views/topic_status_view.js => components/topic_status_component.js} (79%) diff --git a/app/assets/javascripts/discourse/views/topic_status_view.js b/app/assets/javascripts/discourse/components/topic_status_component.js similarity index 79% rename from app/assets/javascripts/discourse/views/topic_status_view.js rename to app/assets/javascripts/discourse/components/topic_status_component.js index 08fe459f74a64..4bea06eb01c2e 100644 --- a/app/assets/javascripts/discourse/views/topic_status_view.js +++ b/app/assets/javascripts/discourse/components/topic_status_component.js @@ -1,12 +1,12 @@ /** This view is for rendering an icon representing the status of a topic - @class TopicStatusView - @extends Discourse.View + @class TopicStatusComponent + @extends Ember.Component @namespace Discourse @module Discourse **/ -Discourse.TopicStatusView = Discourse.View.extend({ +Discourse.TopicStatusComponent = Ember.Component.extend({ classNames: ['topic-statuses'], hasDisplayableStatus: Em.computed.or('topic.closed', 'topic.pinned', 'topic.invisible', 'topic.archetypeObject.notDefault'), @@ -15,9 +15,9 @@ Discourse.TopicStatusView = Discourse.View.extend({ render: function(buffer) { if (!this.get('hasDisplayableStatus')) { return; } - var topicStatusView = this; - var renderIconIf = function(conditionProp, name, key) { - if (!topicStatusView.get(conditionProp)) { return; } + var self = this, + renderIconIf = function(conditionProp, name, key) { + if (!self.get(conditionProp)) { return; } var title = I18n.t("topic_statuses." + key + ".help"); buffer.push(""); }; @@ -32,4 +32,4 @@ Discourse.TopicStatusView = Discourse.View.extend({ }); -Discourse.View.registerHelper('topicStatus', Discourse.TopicStatusView); +Discourse.View.registerHelper('topicStatus', Discourse.TopicStatusComponent); diff --git a/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars b/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars index 25950e80e736d..8c11a522678f3 100644 --- a/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/basic_topic_list.js.handlebars @@ -13,9 +13,10 @@ {{i18n activity}} - {{#groupedEach view.topics}} - + {{#each view.topics}} + + {{topicStatus topic=this}} {{{unbound fancy_title}}} {{#if unread}} {{unbound unread}} @@ -55,7 +56,7 @@ {{/if}} - {{/groupedEach}} + {{/each}} {{else}} From 69503345c4a7291455a890db740ef0e9c6036b55 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Nov 2013 13:43:11 -0500 Subject: [PATCH 23/25] FIX: Some more non-english category errors --- .../controllers/edit_category_controller.js | 7 +++---- .../discourse/controllers/list_controller.js | 3 +-- .../javascripts/discourse/models/nav_item.js | 13 ++++++------- .../discourse/views/nav_item_view.js | 19 ++++++++++--------- 4 files changed, 20 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/edit_category_controller.js b/app/assets/javascripts/discourse/controllers/edit_category_controller.js index c2a05af493199..3e73bfdbb35aa 100644 --- a/app/assets/javascripts/discourse/controllers/edit_category_controller.js +++ b/app/assets/javascripts/discourse/controllers/edit_category_controller.js @@ -135,19 +135,18 @@ Discourse.EditCategoryController = Discourse.ObjectController.extend(Discourse.M this.set('saving', true); model.set('parentCategory', parentCategory); + self.set('saving', false); this.get('model').save().then(function(result) { - // success self.send('closeModal'); - model.set('slug', result.category.slug); + model.setProperties({slug: result.category.slug, id: result.category.id }); Discourse.URL.redirectTo("/category/" + Discourse.Category.slugFor(model)); - }).fail(function(error) { + }).fail(function(error) { if (error && error.responseText) { self.flash($.parseJSON(error.responseText).errors[0]); } else { self.flash(I18n.t('generic_error')); } - self.set('saving', false); }); }, diff --git a/app/assets/javascripts/discourse/controllers/list_controller.js b/app/assets/javascripts/discourse/controllers/list_controller.js index 96eae597d2245..232ada03a8f6b 100644 --- a/app/assets/javascripts/discourse/controllers/list_controller.js +++ b/app/assets/javascripts/discourse/controllers/list_controller.js @@ -13,12 +13,11 @@ Discourse.ListController = Discourse.Controller.extend({ needs: ["composer", "modal", "listTopics"], availableNavItems: function() { - var loggedOn = !!Discourse.User.current(); var category = this.get("category"); return Discourse.SiteSettings.top_menu.split("|").map(function(i) { return Discourse.NavItem.fromText(i, { - loggedOn: loggedOn, + loggedOn: !!Discourse.User.current(), category: category }); }).filter(function(i) { diff --git a/app/assets/javascripts/discourse/models/nav_item.js b/app/assets/javascripts/discourse/models/nav_item.js index 0a58954beb8a4..4946677b54f7b 100644 --- a/app/assets/javascripts/discourse/models/nav_item.js +++ b/app/assets/javascripts/discourse/models/nav_item.js @@ -37,17 +37,16 @@ Discourse.NavItem = Discourse.Model.extend({ // href from this item filterMode: function() { var name = this.get('name'); + if( name.split('/')[0] === 'category' ) { - return 'category/' + this.get('categorySlug'); + return 'category/' + this.get('categorySlug') } else { - var mode = ""; - var category = this.get("category"); + var mode = "", + category = this.get("category"); + if(category){ mode += "category/"; - - var parentSlug = category.get('parentCategory.slug'); - if (parentSlug) { mode += parentSlug + "/"; } - mode += category.get("slug") + "/l/"; + mode += Discourse.Category.slugFor(this.get('category')) + "/l/"; } return mode + name.replace(' ', '-'); } diff --git a/app/assets/javascripts/discourse/views/nav_item_view.js b/app/assets/javascripts/discourse/views/nav_item_view.js index 065219acd09d5..143d3475ac401 100644 --- a/app/assets/javascripts/discourse/views/nav_item_view.js +++ b/app/assets/javascripts/discourse/views/nav_item_view.js @@ -17,9 +17,10 @@ Discourse.NavItemView = Discourse.View.extend({ active: Discourse.computed.propertyEqual('content.filterMode', 'controller.filterMode'), title: function() { - var categoryName, extra, name; - name = this.get('content.name'); - categoryName = this.get('content.categoryName'); + var categoryName = this.get('content.categoryName'), + name = this.get('content.name'), + extra; + if (categoryName) { extra = { categoryName: categoryName }; name = "category"; @@ -29,12 +30,12 @@ Discourse.NavItemView = Discourse.View.extend({ name: function() { - var categoryName, extra, name; - name = this.get('content.name'); - categoryName = this.get('content.categoryName'); - extra = { - count: this.get('content.count') || 0 - }; + var categoryName = this.get('content.categoryName'), + name = this.get('content.name'), + extra = { + count: this.get('content.count') || 0 + }; + if (categoryName) { name = 'category'; extra.categoryName = Discourse.Formatter.toTitleCase(categoryName); From cc47ade2ed67961194ac37bdaa0d21ee35ad7bd5 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Nov 2013 13:59:31 -0500 Subject: [PATCH 24/25] missing semi colon --- app/assets/javascripts/discourse/models/nav_item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/nav_item.js b/app/assets/javascripts/discourse/models/nav_item.js index 4946677b54f7b..fcba5f39aac32 100644 --- a/app/assets/javascripts/discourse/models/nav_item.js +++ b/app/assets/javascripts/discourse/models/nav_item.js @@ -39,7 +39,7 @@ Discourse.NavItem = Discourse.Model.extend({ var name = this.get('name'); if( name.split('/')[0] === 'category' ) { - return 'category/' + this.get('categorySlug') + return 'category/' + this.get('categorySlug'); } else { var mode = "", category = this.get("category"); From b8e63719f85299b1c4fbe431e8b2278118e2448f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Nov 2013 14:24:40 -0500 Subject: [PATCH 25/25] FIX: Don't autolink within a markdown link. --- .../javascripts/discourse/dialects/autolink_dialect.js | 4 ++++ test/javascripts/lib/markdown_test.js | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/app/assets/javascripts/discourse/dialects/autolink_dialect.js b/app/assets/javascripts/discourse/dialects/autolink_dialect.js index 85caabb705353..f3c8c10e6f533 100644 --- a/app/assets/javascripts/discourse/dialects/autolink_dialect.js +++ b/app/assets/javascripts/discourse/dialects/autolink_dialect.js @@ -10,6 +10,10 @@ var urlReplacerArgs = { var url = matches[1], displayUrl = url; + + // Don't autolink a markdown link to something + if (url.match(/\]\[\d$/)) { return; } + // If we improperly caught a markdown link abort if (url.match(/\(http/)) { return; } diff --git a/test/javascripts/lib/markdown_test.js b/test/javascripts/lib/markdown_test.js index 70795be41c001..c5cdc61082322 100644 --- a/test/javascripts/lib/markdown_test.js +++ b/test/javascripts/lib/markdown_test.js @@ -261,6 +261,12 @@ test("Oneboxing", function() { }); +test("links with full urls", function() { + cooked("[http://eviltrout.com][1] is a url\n\n[1]: http://eviltrout.com", + "

http://eviltrout.com is a url

", + "it supports links that are full URLs"); +}); + test("Code Blocks", function() { cooked("```\na\nb\nc\n\nd\n```",