diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..037403136d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +app/assets/stylesheets/vendor/ +plugins/**/assets/stylesheets/vendor/ diff --git a/.travis.yml b/.travis.yml index 16ccc5722b..8924dce113 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,6 +55,7 @@ before_install: - git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign - git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon - git clone --depth=1 https://github.com/discourse/discourse-staff-notes.git plugins/discourse-staff-notes + - git clone --depth=1 https://github.com/discourse/discourse-group-tracker - export PATH=$HOME/.yarn/bin:$PATH install: diff --git a/Gemfile b/Gemfile index f5a03d7d5d..f13983aad6 100644 --- a/Gemfile +++ b/Gemfile @@ -13,13 +13,13 @@ if rails_master? gem 'rails', git: 'https://github.com/rails/rails.git' gem 'seed-fu', git: 'https://github.com/SamSaffron/seed-fu.git', branch: 'discourse' else - gem 'actionmailer', '~> 5.1' - gem 'actionpack', '~> 5.1' - gem 'actionview', '~> 5.1' - gem 'activemodel', '~> 5.1' - gem 'activerecord', '~> 5.1' - gem 'activesupport', '~> 5.1' - gem 'railties', '~> 5.1' + gem 'actionmailer', '5.2' + gem 'actionpack', '5.2' + gem 'actionview', '5.2' + gem 'activemodel', '5.2' + gem 'activerecord', '5.2' + gem 'activesupport', '5.2' + gem 'railties', '5.2' gem 'sprockets-rails' gem 'seed-fu' end @@ -34,7 +34,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.48' +gem 'onebox', '1.8.50' gem 'http_accept_language', '~>2.0.5', require: false @@ -78,7 +78,8 @@ gem 'omniauth-oauth2', require: false gem 'omniauth-google-oauth2' gem 'oj' -gem 'pg', '~> 0.21.0' +gem 'pg' +gem 'mini_sql' gem 'pry-rails', require: false gem 'r2', '~> 0.2.5', require: false gem 'rake' @@ -184,6 +185,8 @@ if ENV["IMPORT"] == "1" gem 'sqlite3', '~> 1.3.13' gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md' gem 'reverse_markdown' + gem 'tiny_tds' end gem 'webpush', require: false +gem 'exifr', '1.2.5' diff --git a/Gemfile.lock b/Gemfile.lock index 428ad9f536..ebc8564386 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,86 +1,88 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (5.1.4) - actionpack (= 5.1.4) - actionview (= 5.1.4) - activejob (= 5.1.4) + actionmailer (5.2.0) + actionpack (= 5.2.0) + actionview (= 5.2.0) + activejob (= 5.2.0) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.4) - actionview (= 5.1.4) - activesupport (= 5.1.4) + actionpack (5.2.0) + actionview (= 5.2.0) + activesupport (= 5.2.0) rack (~> 2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.4) - activesupport (= 5.1.4) + actionview (5.2.0) + activesupport (= 5.2.0) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - active_model_serializers (0.8.3) + active_model_serializers (0.8.4) activemodel (>= 3.0) - activejob (5.1.4) - activesupport (= 5.1.4) + activejob (5.2.0) + activesupport (= 5.2.0) globalid (>= 0.3.6) - activemodel (5.1.4) - activesupport (= 5.1.4) - activerecord (5.1.4) - activemodel (= 5.1.4) - activesupport (= 5.1.4) - arel (~> 8.0) - activesupport (5.1.4) + activemodel (5.2.0) + activesupport (= 5.2.0) + activerecord (5.2.0) + activemodel (= 5.2.0) + activesupport (= 5.2.0) + arel (>= 9.0) + activesupport (5.2.0) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) + i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.5.1) - public_suffix (~> 2.0, >= 2.0.2) - annotate (2.7.2) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + annotate (2.7.4) activerecord (>= 3.2, < 6.0) rake (>= 10.4, < 13.0) - arel (8.0.0) + arel (9.0.0) ast (2.4.0) - aws-partitions (1.24.0) - aws-sdk-core (3.6.0) + aws-eventstream (1.0.1) + aws-partitions (1.92.0) + aws-sdk-core (3.21.2) + aws-eventstream (~> 1.0) aws-partitions (~> 1.0) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-kms (1.2.0) + aws-sdk-kms (1.5.0) aws-sdk-core (~> 3) aws-sigv4 (~> 1.0) - aws-sdk-s3 (1.4.0) - aws-sdk-core (~> 3) + aws-sdk-s3 (1.14.0) + aws-sdk-core (~> 3, >= 3.21.2) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.0) aws-sigv4 (1.0.2) - barber (0.11.2) - ember-source (>= 1.0, < 3) + barber (0.12.0) + ember-source (>= 1.0, < 3.1) execjs (>= 1.2, < 3) - better_errors (2.1.1) + better_errors (2.4.0) coderay (>= 1.0.0) - erubis (>= 2.6.6) + erubi (>= 1.0.0) rack (>= 0.9.0) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.1.8) + bootsnap (1.3.0) msgpack (~> 1.0) builder (3.2.3) - bullet (5.5.1) + bullet (5.7.5) activesupport (>= 3.0.0) - uniform_notifier (~> 1.10.0) - byebug (9.0.6) + uniform_notifier (~> 1.11.0) + byebug (10.0.2) certified (1.0.0) - chunky_png (1.3.8) + chunky_png (1.3.10) coderay (1.1.2) concurrent-ruby (1.0.5) - connection_pool (2.2.1) + connection_pool (2.2.2) cppjieba_rb (0.3.0) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.3) + crass (1.0.4) debug_inspector (0.0.3) diff-lcs (1.3) discourse_image_optim (0.24.5) @@ -90,8 +92,8 @@ GEM in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) email_reply_trimmer (0.1.12) - ember-data-source (2.2.1) - ember-source (>= 1.8, < 3.0) + ember-data-source (3.0.2) + ember-source (>= 2, < 3.0) ember-handlebars-template (0.7.5) barber (>= 0.11.0) sprockets (>= 3.3, < 4) @@ -103,59 +105,60 @@ GEM jquery-rails (>= 1.0.17) railties (>= 3.1) ember-source (2.13.3) - erubi (1.6.1) - erubis (2.7.0) - excon (0.56.0) + erubi (1.7.1) + excon (0.62.0) execjs (2.7.0) exifr (1.2.5) - fabrication (2.9.8) + fabrication (2.20.1) fakeweb (1.3.0) - faraday (0.11.0) + faraday (0.12.2) multipart-post (>= 1.2, < 3) fast_blank (1.0.0) fast_xor (1.1.3) rake rake-compiler fast_xs (0.8.0) - fastimage (2.1.1) - ffi (1.9.18) + fastimage (2.1.3) + ffi (1.9.25) flamegraph (0.9.5) - foreman (0.84.0) + foreman (0.85.0) thor (~> 0.19.1) fspath (3.1.0) gc_tracer (1.5.1) - globalid (0.4.0) + globalid (0.4.1) activesupport (>= 4.2.0) guess_html_encoding (0.0.11) - hashdiff (0.3.4) - hashie (3.5.5) - highline (1.7.8) + hashdiff (0.3.7) + hashie (3.5.7) + highline (2.0.0) hiredis (0.6.1) hkdf (0.3.0) htmlentities (4.3.4) http_accept_language (2.0.5) - i18n (0.8.6) + i18n (1.0.1) + concurrent-ruby (~> 1.0) image_size (1.5.0) - in_threads (1.4.0) - jmespath (1.3.1) - jquery-rails (4.3.1) + in_threads (1.5.0) + jaro_winkler (1.5.1) + jmespath (1.4.0) + jquery-rails (4.3.3) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jwt (1.5.6) - kgio (2.11.1) + kgio (2.11.2) libv8 (6.3.292.48.1) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - lograge (0.7.1) - actionpack (>= 4, < 5.2) - activesupport (>= 4, < 5.2) - railties (>= 4, < 5.2) + lograge (0.10.0) + actionpack (>= 4) + activesupport (>= 4) + railties (>= 4) request_store (~> 1.0) logstash-event (1.2.02) - logstash-logger (0.25.1) + logstash-logger (0.26.1) logstash-event (~> 1.2) logster (1.2.9) loofah (2.2.2) @@ -173,59 +176,59 @@ GEM mini_portile2 (2.3.0) mini_racer (0.1.15) libv8 (~> 6.3) + mini_sql (0.1.9) mini_suffix (0.3.0) ffi (~> 1.9) - minitest (5.10.3) - mocha (1.2.1) + minitest (5.11.3) + mocha (1.5.0) metaclass (~> 0.0.1) - mock_redis (0.17.3) + mock_redis (0.18.0) moneta (1.0.0) msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) mustache (1.0.5) - nokogiri (1.8.2) + nokogiri (1.8.3) mini_portile2 (~> 2.3.0) nokogumbo (1.5.0) nokogiri - oauth (0.5.1) - oauth2 (1.3.1) - faraday (>= 0.8, < 0.12) + oauth (0.5.4) + oauth2 (1.4.0) + faraday (>= 0.8, < 0.13) jwt (~> 1.0) multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - oj (3.4.0) - omniauth (1.6.1) + oj (3.6.2) + omniauth (1.8.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) - omniauth-facebook (4.0.0) + omniauth-facebook (5.0.0) omniauth-oauth2 (~> 1.2) omniauth-github (1.3.0) omniauth (~> 1.5) omniauth-oauth2 (>= 1.4.0, < 2.0) - omniauth-google-oauth2 (0.3.1) - jwt (~> 1.0) - multi_json (~> 1.3) + omniauth-google-oauth2 (0.5.3) + jwt (>= 1.5) omniauth (>= 1.1.1) - omniauth-oauth2 (>= 1.3.1) - omniauth-instagram (1.0.2) + omniauth-oauth2 (>= 1.5) + omniauth-instagram (1.3.0) omniauth (~> 1) omniauth-oauth2 (~> 1) omniauth-oauth (1.1.0) oauth omniauth (~> 1.0) - omniauth-oauth2 (1.4.0) - oauth2 (~> 1.0) + omniauth-oauth2 (1.5.0) + oauth2 (~> 1.1) omniauth (~> 1.2) omniauth-openid (1.0.1) omniauth (~> 1.0) rack-openid (~> 1.3.1) - omniauth-twitter (1.3.0) + omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.48) + onebox (1.8.50) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -236,54 +239,54 @@ GEM redis ruby-openid parallel (1.12.1) - parser (2.5.0.3) + parser (2.5.1.0) ast (~> 2.4.0) - pg (0.21.0) - powerpack (0.1.1) - progress (3.3.1) + pg (1.0.0) + powerpack (0.1.2) + progress (3.4.0) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) pry-nav (0.2.4) pry (>= 0.9.10, < 0.11.0) - pry-rails (0.3.4) - pry (>= 0.9.10) - public_suffix (2.0.5) - puma (3.9.1) - r2 (0.2.6) + pry-rails (0.3.6) + pry (>= 0.10.4) + public_suffix (3.0.2) + puma (3.11.4) + r2 (0.2.7) rack (2.0.5) rack-mini-profiler (1.0.0) rack (>= 1.2.0) rack-openid (1.3.1) rack (>= 1.1.0) ruby-openid (>= 2.1.8) - rack-protection (2.0.1) + rack-protection (2.0.3) rack - rack-test (0.7.0) + rack-test (1.0.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - rails_multisite (2.0.4) + rails_multisite (2.0.5) activerecord (> 4.2, < 6) railties (> 4.2, < 6) - railties (5.1.4) - actionpack (= 5.1.4) - activesupport (= 5.1.4) + railties (5.2.0) + actionpack (= 5.2.0) + activesupport (= 5.2.0) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (3.0.0) raindrops (0.19.0) - rake (12.3.0) + rake (12.3.1) rake-compiler (1.0.4) rake - rb-fsevent (0.9.8) - rb-inotify (0.9.8) - ffi (>= 0.5.0) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) rbtrace (0.4.10) ffi (>= 1.0.6) msgpack (>= 0.4.3) @@ -291,37 +294,39 @@ GEM redis (4.0.1) redis-namespace (1.6.0) redis (>= 3.0.4) - request_store (1.3.2) - rinku (2.0.2) - rotp (3.3.0) + request_store (1.4.1) + rack (>= 1.4) + rinku (2.0.4) + rotp (3.3.1) rqrcode (0.10.1) chunky_png (~> 1.0) - rspec (3.6.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) + rspec-support (~> 3.7.0) rspec-html-matchers (0.9.1) nokogiri (~> 1) rspec (>= 3.0.0.a, < 4) - rspec-mocks (3.6.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-rails (3.6.1) + rspec-support (~> 3.7.0) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) - rspec-support (3.6.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.1) rtlit (0.0.5) - rubocop (0.53.0) + rubocop (0.57.2) + jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.5) powerpack (~> 0.1) @@ -329,23 +334,27 @@ GEM ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) ruby-openid (2.7.0) - ruby-prof (0.16.2) + ruby-prof (0.17.0) ruby-progressbar (1.9.0) ruby-readability (0.7.0) guess_html_encoding (>= 0.0.4) nokogiri (>= 1.6.0) ruby_dep (1.5.0) safe_yaml (1.0.4) - sanitize (4.6.4) + sanitize (4.6.5) crass (~> 1.0.2) nokogiri (>= 1.4.4) nokogumbo (~> 1.4) - sass (3.4.24) - sassc (1.11.2) + sass (3.5.6) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sassc (1.11.4) bundler ffi (~> 1.9.6) sass (>= 3.3.0) - seed-fu (2.3.7) + seed-fu (2.3.9) activerecord (>= 3.1) activesupport (>= 3.1) shoulda (3.5.0) @@ -363,29 +372,29 @@ GEM sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.2.0) + sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) sshkey (1.9.0) - stackprof (0.2.10) + stackprof (0.2.11) thor (0.19.4) thread_safe (0.3.6) - tilt (2.0.7) + tilt (2.0.8) trollop (2.1.2) - tzinfo (1.2.3) + tzinfo (1.2.5) thread_safe (~> 0.1) - uglifier (3.2.0) + uglifier (4.1.11) execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext - unf_ext (0.0.7.4) - unicode-display_width (1.3.0) + unf_ext (0.0.7.5) + unicode-display_width (1.4.0) unicorn (5.4.0) kgio (~> 2.6) raindrops (~> 0.7) - uniform_notifier (1.10.0) - webmock (3.0.1) + uniform_notifier (1.11.0) + webmock (3.4.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff @@ -397,13 +406,13 @@ PLATFORMS ruby DEPENDENCIES - actionmailer (~> 5.1) - actionpack (~> 5.1) - actionview (~> 5.1) + actionmailer (= 5.2) + actionpack (= 5.2) + actionview (= 5.2) active_model_serializers (~> 0.8.3) - activemodel (~> 5.1) - activerecord (~> 5.1) - activesupport (~> 5.1) + activemodel (= 5.2) + activerecord (= 5.2) + activesupport (= 5.2) annotate aws-sdk-s3 barber @@ -421,6 +430,7 @@ DEPENDENCIES ember-source (= 2.13.3) excon execjs + exifr (= 1.2.5) fabrication fakeweb (~> 1.3.0) fast_blank @@ -445,6 +455,7 @@ DEPENDENCIES message_bus mini_mime mini_racer + mini_sql mini_suffix minitest mocha @@ -461,9 +472,9 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.48) + onebox (= 1.8.50) openid-redis-store - pg (~> 0.21.0) + pg pry-nav pry-rails puma @@ -471,7 +482,7 @@ DEPENDENCIES rack-mini-profiler rack-protection rails_multisite - railties (~> 5.1) + railties (= 5.2) rake rb-fsevent rb-inotify (~> 0.9) @@ -505,4 +516,4 @@ DEPENDENCIES webpush BUNDLED WITH - 1.16.1 + 1.16.2 diff --git a/app/assets/javascripts/admin/adapters/build-plugin.js.es6 b/app/assets/javascripts/admin/adapters/build-plugin.js.es6 index b73bf7ecb1..0297aaa8be 100644 --- a/app/assets/javascripts/admin/adapters/build-plugin.js.es6 +++ b/app/assets/javascripts/admin/adapters/build-plugin.js.es6 @@ -1,9 +1,11 @@ -import RestAdapter from 'discourse/adapters/rest'; +import RestAdapter from "discourse/adapters/rest"; export default function buildPluginAdapter(pluginName) { return RestAdapter.extend({ pathFor(store, type, findArgs) { - return "/admin/plugins/" + pluginName + this._super(store, type, findArgs); + return ( + "/admin/plugins/" + pluginName + this._super(store, type, findArgs) + ); } }); } diff --git a/app/assets/javascripts/admin/adapters/customization-base.js.es6 b/app/assets/javascripts/admin/adapters/customization-base.js.es6 index f57240a116..d1087e7019 100644 --- a/app/assets/javascripts/admin/adapters/customization-base.js.es6 +++ b/app/assets/javascripts/admin/adapters/customization-base.js.es6 @@ -1,4 +1,4 @@ -import RestAdapter from 'discourse/adapters/rest'; +import RestAdapter from "discourse/adapters/rest"; export default RestAdapter.extend({ basePath() { diff --git a/app/assets/javascripts/admin/adapters/embedding.js.es6 b/app/assets/javascripts/admin/adapters/embedding.js.es6 index c8985cfdca..c1d5eceace 100644 --- a/app/assets/javascripts/admin/adapters/embedding.js.es6 +++ b/app/assets/javascripts/admin/adapters/embedding.js.es6 @@ -1,4 +1,4 @@ -import RestAdapter from 'discourse/adapters/rest'; +import RestAdapter from "discourse/adapters/rest"; export default RestAdapter.extend({ pathFor() { diff --git a/app/assets/javascripts/admin/adapters/flagged-post.js.es6 b/app/assets/javascripts/admin/adapters/flagged-post.js.es6 index 1a1065f638..dc44315170 100644 --- a/app/assets/javascripts/admin/adapters/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/adapters/flagged-post.js.es6 @@ -1,4 +1,4 @@ -import RestAdapter from 'discourse/adapters/rest'; +import RestAdapter from "discourse/adapters/rest"; export default RestAdapter.extend({ pathFor(store, type, findArgs) { @@ -17,20 +17,20 @@ export default RestAdapter.extend({ hasMore: pa.conversation.has_more, response: { excerpt: pa.conversation.response.excerpt, - user: helper.lookup('user', pa.conversation.response.user_id) + user: helper.lookup("user", pa.conversation.response.user_id) } }; if (pa.conversation.reply) { conversation.reply = { excerpt: pa.conversation.reply.excerpt, - user: helper.lookup('user', pa.conversation.reply.user_id) + user: helper.lookup("user", pa.conversation.reply.user_id) }; } conversations.push(conversation); } }); - flag.set('conversations', conversations); + flag.set("conversations", conversations); }); return results; diff --git a/app/assets/javascripts/admin/adapters/site-text.js.es6 b/app/assets/javascripts/admin/adapters/site-text.js.es6 index b547b06f3c..f362b06fe1 100644 --- a/app/assets/javascripts/admin/adapters/site-text.js.es6 +++ b/app/assets/javascripts/admin/adapters/site-text.js.es6 @@ -1,2 +1,2 @@ -import CustomizationBase from 'admin/adapters/customization-base'; +import CustomizationBase from "admin/adapters/customization-base"; export default CustomizationBase; diff --git a/app/assets/javascripts/admin/adapters/theme.js.es6 b/app/assets/javascripts/admin/adapters/theme.js.es6 index df9c8830d1..b98ba3e423 100644 --- a/app/assets/javascripts/admin/adapters/theme.js.es6 +++ b/app/assets/javascripts/admin/adapters/theme.js.es6 @@ -1,4 +1,4 @@ -import RestAdapter from 'discourse/adapters/rest'; +import RestAdapter from "discourse/adapters/rest"; export default RestAdapter.extend({ basePath() { @@ -7,7 +7,9 @@ export default RestAdapter.extend({ afterFindAll(results) { let map = {}; - results.forEach(theme => {map[theme.id] = theme;}); + results.forEach(theme => { + map[theme.id] = theme; + }); results.forEach(theme => { let mapped = theme.get("child_themes") || []; mapped = mapped.map(t => map[t.id]); diff --git a/app/assets/javascripts/admin/adapters/user-field.js.es6 b/app/assets/javascripts/admin/adapters/user-field.js.es6 index b547b06f3c..f362b06fe1 100644 --- a/app/assets/javascripts/admin/adapters/user-field.js.es6 +++ b/app/assets/javascripts/admin/adapters/user-field.js.es6 @@ -1,2 +1,2 @@ -import CustomizationBase from 'admin/adapters/customization-base'; +import CustomizationBase from "admin/adapters/customization-base"; export default CustomizationBase; diff --git a/app/assets/javascripts/admin/adapters/web-hook-event.js.es6 b/app/assets/javascripts/admin/adapters/web-hook-event.js.es6 index 122070ce3e..4c80ac3ff0 100644 --- a/app/assets/javascripts/admin/adapters/web-hook-event.js.es6 +++ b/app/assets/javascripts/admin/adapters/web-hook-event.js.es6 @@ -1,7 +1,7 @@ -import RESTAdapter from 'discourse/adapters/rest'; +import RESTAdapter from "discourse/adapters/rest"; export default RESTAdapter.extend({ basePath() { - return '/admin/api/'; + return "/admin/api/"; } }); diff --git a/app/assets/javascripts/admin/adapters/web-hook.js.es6 b/app/assets/javascripts/admin/adapters/web-hook.js.es6 index 122070ce3e..4c80ac3ff0 100644 --- a/app/assets/javascripts/admin/adapters/web-hook.js.es6 +++ b/app/assets/javascripts/admin/adapters/web-hook.js.es6 @@ -1,7 +1,7 @@ -import RESTAdapter from 'discourse/adapters/rest'; +import RESTAdapter from "discourse/adapters/rest"; export default RESTAdapter.extend({ basePath() { - return '/admin/api/'; + return "/admin/api/"; } }); diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index faad36798a..192bc4b410 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -1,37 +1,37 @@ -import loadScript from 'discourse/lib/load-script'; -import { observes } from 'ember-addons/ember-computed-decorators'; +import loadScript from "discourse/lib/load-script"; +import { observes } from "ember-addons/ember-computed-decorators"; const LOAD_ASYNC = !Ember.testing; export default Ember.Component.extend({ - mode: 'css', - classNames: ['ace-wrapper'], + mode: "css", + classNames: ["ace-wrapper"], _editor: null, _skipContentChangeEvent: null, disabled: false, - @observes('editorId') + @observes("editorId") editorIdChanged() { - if (this.get('autofocus')) { - this.send('focus'); + if (this.get("autofocus")) { + this.send("focus"); } }, - @observes('content') + @observes("content") contentChanged() { if (this._editor && !this._skipContentChangeEvent) { - this._editor.getSession().setValue(this.get('content')); + this._editor.getSession().setValue(this.get("content")); } }, - @observes('mode') + @observes("mode") modeChanged() { if (LOAD_ASYNC && this._editor && !this._skipContentChangeEvent) { - this._editor.getSession().setMode("ace/mode/" + this.get('mode')); + this._editor.getSession().setMode("ace/mode/" + this.get("mode")); } }, - @observes('disabled') + @observes("disabled") disabledStateChanged() { this.changeDisabledState(); }, @@ -39,7 +39,7 @@ export default Ember.Component.extend({ changeDisabledState() { const editor = this._editor; if (editor) { - const disabled = this.get('disabled'); + const disabled = this.get("disabled"); editor.setOptions({ readOnly: disabled, highlightActiveLine: !disabled, @@ -56,12 +56,11 @@ export default Ember.Component.extend({ } if (this.appEvents) { // xxx: don't run during qunit tests - this.appEvents.off('ace:resize', this, this.resize); + this.appEvents.off("ace:resize", this, this.resize); } - $(window).off('ace:resize'); - - }.on('willDestroyElement'), + $(window).off("ace:resize"); + }.on("willDestroyElement"), resize() { if (this._editor) { @@ -73,37 +72,41 @@ export default Ember.Component.extend({ this._super(); loadScript("/javascripts/ace/ace.js", { scriptTag: true }).then(() => { - window.ace.require(['ace/ace'], loadedAce => { - if (!this.element || this.isDestroying || this.isDestroyed) { return; } - const editor = loadedAce.edit(this.$('.ace')[0]); + window.ace.require(["ace/ace"], loadedAce => { + if (!this.element || this.isDestroying || this.isDestroyed) { + return; + } + const editor = loadedAce.edit(this.$(".ace")[0]); if (LOAD_ASYNC) { editor.setTheme("ace/theme/chrome"); } editor.setShowPrintMargin(false); - editor.setOptions({fontSize: "14px"}); + editor.setOptions({ fontSize: "14px" }); if (LOAD_ASYNC) { - editor.getSession().setMode("ace/mode/" + this.get('mode')); + editor.getSession().setMode("ace/mode/" + this.get("mode")); } - editor.on('change', () => { + editor.on("change", () => { this._skipContentChangeEvent = true; - this.set('content', editor.getSession().getValue()); + this.set("content", editor.getSession().getValue()); this._skipContentChangeEvent = false; }); editor.$blockScrolling = Infinity; - editor.renderer.setScrollMargin(10,10); + editor.renderer.setScrollMargin(10, 10); - this.$().data('editor', editor); + this.$().data("editor", editor); this._editor = editor; this.changeDisabledState(); - $(window).off('ace:resize').on('ace:resize', ()=>{ - this.appEvents.trigger('ace:resize'); - }); + $(window) + .off("ace:resize") + .on("ace:resize", () => { + this.appEvents.trigger("ace:resize"); + }); if (this.appEvents) { // xxx: don't run during qunit tests - this.appEvents.on('ace:resize', ()=>this.resize()); + this.appEvents.on("ace:resize", () => this.resize()); } if (this.get("autofocus")) { diff --git a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 index 01f4547b17..c2f9df5205 100644 --- a/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/components/admin-backups-logs.js.es6 @@ -1,57 +1,64 @@ -import debounce from 'discourse/lib/debounce'; -import { renderSpinner } from 'discourse/helpers/loading-spinner'; -import { escapeExpression } from 'discourse/lib/utilities'; -import { bufferedRender } from 'discourse-common/lib/buffered-render'; +import debounce from "discourse/lib/debounce"; +import { renderSpinner } from "discourse/helpers/loading-spinner"; +import { escapeExpression } from "discourse/lib/utilities"; +import { bufferedRender } from "discourse-common/lib/buffered-render"; -export default Ember.Component.extend(bufferedRender({ - classNames: ["admin-backups-logs"], +export default Ember.Component.extend( + bufferedRender({ + classNames: ["admin-backups-logs"], - init() { - this._super(); - this._reset(); - }, + init() { + this._super(); + this._reset(); + }, - _reset() { - this.setProperties({ formattedLogs: "", index: 0 }); - }, + _reset() { + this.setProperties({ formattedLogs: "", index: 0 }); + }, - _scrollDown() { - const $div = this.$()[0]; - $div.scrollTop = $div.scrollHeight; - }, + _scrollDown() { + const $div = this.$()[0]; + $div.scrollTop = $div.scrollHeight; + }, - _updateFormattedLogs: debounce(function() { - const logs = this.get("logs"); - if (logs.length === 0) { - this._reset(); // reset the cached logs whenever the model is reset - } else { - // do the log formatting only once for HELLish performance - let formattedLogs = this.get("formattedLogs"); - for (let i = this.get("index"), length = logs.length; i < length; i++) { - const date = logs[i].get("timestamp"), - message = escapeExpression(logs[i].get("message")); - formattedLogs += "[" + date + "] " + message + "\n"; + _updateFormattedLogs: debounce(function() { + const logs = this.get("logs"); + if (logs.length === 0) { + this._reset(); // reset the cached logs whenever the model is reset + } else { + // do the log formatting only once for HELLish performance + let formattedLogs = this.get("formattedLogs"); + for (let i = this.get("index"), length = logs.length; i < length; i++) { + const date = logs[i].get("timestamp"), + message = escapeExpression(logs[i].get("message")); + formattedLogs += "[" + date + "] " + message + "\n"; + } + // update the formatted logs & cache index + this.setProperties({ + formattedLogs: formattedLogs, + index: logs.length + }); + // force rerender + this.rerenderBuffer(); } - // update the formatted logs & cache index - this.setProperties({ formattedLogs: formattedLogs, index: logs.length }); - // force rerender - this.rerenderBuffer(); - } - Ember.run.scheduleOnce('afterRender', this, this._scrollDown); - }, 150).observes("logs.[]").on('init'), + Ember.run.scheduleOnce("afterRender", this, this._scrollDown); + }, 150) + .observes("logs.[]") + .on("init"), - buildBuffer(buffer) { - const formattedLogs = this.get("formattedLogs"); - if (formattedLogs && formattedLogs.length > 0) { - buffer.push("
");
-      buffer.push(formattedLogs);
-      buffer.push("
"); - } else { - buffer.push("

" + I18n.t("admin.backups.logs.none") + "

"); + buildBuffer(buffer) { + const formattedLogs = this.get("formattedLogs"); + if (formattedLogs && formattedLogs.length > 0) { + buffer.push("
");
+        buffer.push(formattedLogs);
+        buffer.push("
"); + } else { + buffer.push("

" + I18n.t("admin.backups.logs.none") + "

"); + } + // add a loading indicator + if (this.get("status.isOperationRunning")) { + buffer.push(renderSpinner("small")); + } } - // add a loading indicator - if (this.get("status.isOperationRunning")) { - buffer.push(renderSpinner('small')); - } - } -})); + }) +); diff --git a/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 b/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 index 8d43474a9d..add82cbbe7 100644 --- a/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 +++ b/app/assets/javascripts/admin/components/admin-directory-toggle.js.es6 @@ -1,33 +1,37 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; -import { bufferedRender } from 'discourse-common/lib/buffered-render'; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { bufferedRender } from "discourse-common/lib/buffered-render"; -export default Ember.Component.extend(bufferedRender({ - tagName: 'th', - classNames: ['sortable'], - rerenderTriggers: ['order', 'ascending'], +export default Ember.Component.extend( + bufferedRender({ + tagName: "th", + classNames: ["sortable"], + rerenderTriggers: ["order", "ascending"], - buildBuffer(buffer) { - const icon = this.get('icon'); + buildBuffer(buffer) { + const icon = this.get("icon"); - if (icon) { - buffer.push(iconHTML(icon)); + if (icon) { + buffer.push(iconHTML(icon)); + } + + buffer.push(I18n.t(this.get("i18nKey"))); + + if (this.get("field") === this.get("order")) { + buffer.push( + iconHTML(this.get("ascending") ? "chevron-up" : "chevron-down") + ); + } + }, + + click() { + const currentOrder = this.get("order"); + const field = this.get("field"); + + if (currentOrder === field) { + this.set("ascending", this.get("ascending") ? null : true); + } else { + this.setProperties({ order: field, ascending: null }); + } } - - buffer.push(I18n.t(this.get('i18nKey'))); - - if (this.get('field') === this.get('order')) { - buffer.push(iconHTML(this.get('ascending') ? 'chevron-up' : 'chevron-down')); - } - }, - - click() { - const currentOrder = this.get('order'); - const field = this.get('field'); - - if (currentOrder === field) { - this.set('ascending', this.get('ascending') ? null : true); - } else { - this.setProperties({ order: field, ascending: null }); - } - } -})); + }) +); diff --git a/app/assets/javascripts/admin/components/admin-form-row.js.es6 b/app/assets/javascripts/admin/components/admin-form-row.js.es6 index e7cef2edb0..5159168c30 100644 --- a/app/assets/javascripts/admin/components/admin-form-row.js.es6 +++ b/app/assets/javascripts/admin/components/admin-form-row.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - classNames: ['row'] + classNames: ["row"] }); diff --git a/app/assets/javascripts/admin/components/admin-graph.js.es6 b/app/assets/javascripts/admin/components/admin-graph.js.es6 index 724e967374..7752230cfb 100644 --- a/app/assets/javascripts/admin/components/admin-graph.js.es6 +++ b/app/assets/javascripts/admin/components/admin-graph.js.es6 @@ -1,49 +1,56 @@ -import loadScript from 'discourse/lib/load-script'; -import { number } from 'discourse/lib/formatter'; +import loadScript from "discourse/lib/load-script"; +import { number } from "discourse/lib/formatter"; export default Ember.Component.extend({ - tagName: 'canvas', - refreshChart(){ + tagName: "canvas", + refreshChart() { const ctx = this.$()[0].getContext("2d"); const model = this.get("model"); const rawData = this.get("model.data"); var data = { labels: rawData.map(r => r.x), - datasets: [{ - data: rawData.map(r => r.y), - label: model.get('title'), - backgroundColor: "rgba(200,220,240,0.3)", - borderColor: "#08C" - }] + datasets: [ + { + data: rawData.map(r => r.y), + label: model.get("title"), + backgroundColor: "rgba(200,220,240,0.3)", + borderColor: "#08C" + } + ] }; const config = { - type: 'line', + type: "line", data: data, options: { responsive: true, tooltips: { callbacks: { - title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL") + title: context => + moment(context[0].xLabel, "YYYY-MM-DD").format("LL") } }, scales: { - yAxes: [{ - display: true, - ticks: { - callback: (label) => number(label), - suggestedMin: 0 + yAxes: [ + { + display: true, + ticks: { + callback: label => number(label), + suggestedMin: 0 + } } - }] + ] } - }, + } }; this._chart = new window.Chart(ctx, config); }, - didInsertElement(){ - loadScript("/javascripts/Chart.min.js").then(() => this.refreshChart.apply(this)); + didInsertElement() { + loadScript("/javascripts/Chart.min.js").then(() => + this.refreshChart.apply(this) + ); } }); diff --git a/app/assets/javascripts/admin/components/admin-nav.js.es6 b/app/assets/javascripts/admin/components/admin-nav.js.es6 index 9250c1ae73..91ad923ffc 100644 --- a/app/assets/javascripts/admin/components/admin-nav.js.es6 +++ b/app/assets/javascripts/admin/components/admin-nav.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: '' + tagName: "" }); diff --git a/app/assets/javascripts/admin/components/admin-report-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-counts.js.es6 index 1739a186b3..849d81460a 100644 --- a/app/assets/javascripts/admin/components/admin-report-counts.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-counts.js.es6 @@ -1,6 +1,9 @@ export default Ember.Component.extend({ allTime: true, - tagName: 'tr', - reverseColors: Ember.computed.match('report.type', /^(time_to_first_response|topics_with_no_response)$/), - classNameBindings: ['reverseColors'] + tagName: "tr", + reverseColors: Ember.computed.match( + "report.type", + /^(time_to_first_response|topics_with_no_response)$/ + ), + classNameBindings: ["reverseColors"] }); diff --git a/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 index 25bf94db21..b7620b66cd 100644 --- a/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-per-day-counts.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: 'tr' + tagName: "tr" }); diff --git a/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 b/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 index 25bf94db21..b7620b66cd 100644 --- a/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 +++ b/app/assets/javascripts/admin/components/admin-report-trust-level-counts.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: 'tr' + tagName: "tr" }); diff --git a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 index 907f803aed..64e95cecf9 100644 --- a/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 +++ b/app/assets/javascripts/admin/components/admin-user-field-item.js.es6 @@ -1,97 +1,111 @@ -import UserField from 'admin/models/user-field'; -import { bufferedProperty } from 'discourse/mixins/buffered-content'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import { propertyEqual } from 'discourse/lib/computed'; +import UserField from "admin/models/user-field"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { propertyEqual } from "discourse/lib/computed"; -export default Ember.Component.extend(bufferedProperty('userField'), { - editing: Ember.computed.empty('userField.id'), - classNameBindings: [':user-field'], +export default Ember.Component.extend(bufferedProperty("userField"), { + editing: Ember.computed.empty("userField.id"), + classNameBindings: [":user-field"], - cantMoveUp: propertyEqual('userField', 'firstField'), - cantMoveDown: propertyEqual('userField', 'lastField'), + cantMoveUp: propertyEqual("userField", "firstField"), + cantMoveDown: propertyEqual("userField", "lastField"), userFieldsDescription: function() { - return I18n.t('admin.user_fields.description'); + return I18n.t("admin.user_fields.description"); }.property(), bufferedFieldType: function() { - return UserField.fieldTypeById(this.get('buffered.field_type')); - }.property('buffered.field_type'), + return UserField.fieldTypeById(this.get("buffered.field_type")); + }.property("buffered.field_type"), _focusOnEdit: function() { - if (this.get('editing')) { - Ember.run.scheduleOnce('afterRender', this, '_focusName'); + if (this.get("editing")) { + Ember.run.scheduleOnce("afterRender", this, "_focusName"); } - }.observes('editing').on('didInsertElement'), + } + .observes("editing") + .on("didInsertElement"), _focusName: function() { - $('.user-field-name').select(); + $(".user-field-name").select(); }, fieldName: function() { - return UserField.fieldTypeById(this.get('userField.field_type')).get('name'); - }.property('userField.field_type'), + return UserField.fieldTypeById(this.get("userField.field_type")).get( + "name" + ); + }.property("userField.field_type"), flags: function() { const ret = []; - if (this.get('userField.editable')) { - ret.push(I18n.t('admin.user_fields.editable.enabled')); + if (this.get("userField.editable")) { + ret.push(I18n.t("admin.user_fields.editable.enabled")); } - if (this.get('userField.required')) { - ret.push(I18n.t('admin.user_fields.required.enabled')); + if (this.get("userField.required")) { + ret.push(I18n.t("admin.user_fields.required.enabled")); } - if (this.get('userField.show_on_profile')) { - ret.push(I18n.t('admin.user_fields.show_on_profile.enabled')); + if (this.get("userField.show_on_profile")) { + ret.push(I18n.t("admin.user_fields.show_on_profile.enabled")); } - if (this.get('userField.show_on_user_card')) { - ret.push(I18n.t('admin.user_fields.show_on_user_card.enabled')); + if (this.get("userField.show_on_user_card")) { + ret.push(I18n.t("admin.user_fields.show_on_user_card.enabled")); } - return ret.join(', '); - }.property('userField.editable', 'userField.required', 'userField.show_on_profile', 'userField.show_on_user_card'), + return ret.join(", "); + }.property( + "userField.editable", + "userField.required", + "userField.show_on_profile", + "userField.show_on_user_card" + ), actions: { save() { const self = this; - const buffered = this.get('buffered'); - const attrs = buffered.getProperties('name', - 'description', - 'field_type', - 'editable', - 'required', - 'show_on_profile', - 'show_on_user_card', - 'options'); + const buffered = this.get("buffered"); + const attrs = buffered.getProperties( + "name", + "description", + "field_type", + "editable", + "required", + "show_on_profile", + "show_on_user_card", + "options" + ); - this.get('userField').save(attrs).then(function() { - self.set('editing', false); - self.commitBuffer(); - }).catch(popupAjaxError); + this.get("userField") + .save(attrs) + .then(function() { + self.set("editing", false); + self.commitBuffer(); + }) + .catch(popupAjaxError); }, moveUp() { - this.sendAction('moveUpAction', this.get('userField')); + this.sendAction("moveUpAction", this.get("userField")); }, moveDown() { - this.sendAction('moveDownAction', this.get('userField')); + this.sendAction("moveDownAction", this.get("userField")); }, edit() { - this.set('editing', true); + this.set("editing", true); }, destroy() { - this.sendAction('destroyAction', this.get('userField')); + this.sendAction("destroyAction", this.get("userField")); }, cancel() { - const id = this.get('userField.id'); + const id = this.get("userField.id"); if (Ember.isEmpty(id)) { - this.sendAction('destroyAction', this.get('userField')); + this.sendAction("destroyAction", this.get("userField")); } else { this.rollbackBuffer(); - this.set('editing', false); + this.set("editing", false); } } } diff --git a/app/assets/javascripts/admin/components/admin-watched-word.js.es6 b/app/assets/javascripts/admin/components/admin-watched-word.js.es6 index 3c78becb49..d1283e8b3a 100644 --- a/app/assets/javascripts/admin/components/admin-watched-word.js.es6 +++ b/app/assets/javascripts/admin/components/admin-watched-word.js.es6 @@ -1,19 +1,28 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; -import { bufferedRender } from 'discourse-common/lib/buffered-render'; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { bufferedRender } from "discourse-common/lib/buffered-render"; -export default Ember.Component.extend(bufferedRender({ - classNames: ['watched-word'], +export default Ember.Component.extend( + bufferedRender({ + classNames: ["watched-word"], - buildBuffer(buffer) { - buffer.push(iconHTML('times')); - buffer.push(' ' + this.get('word.word')); - }, + buildBuffer(buffer) { + buffer.push(iconHTML("times")); + buffer.push(" " + this.get("word.word")); + }, - click() { - this.get('word').destroy().then(() => { - this.sendAction('action', this.get('word')); - }).catch(e => { - bootbox.alert(I18n.t("generic_error_with_reason", {error: `http: ${e.status} - ${e.body}`})); - });; - } -})); + click() { + this.get("word") + .destroy() + .then(() => { + this.sendAction("action", this.get("word")); + }) + .catch(e => { + bootbox.alert( + I18n.t("generic_error_with_reason", { + error: `http: ${e.status} - ${e.body}` + }) + ); + }); + } + }) +); diff --git a/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js.es6 b/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js.es6 index 976b483995..6e91ce55c7 100644 --- a/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js.es6 +++ b/app/assets/javascripts/admin/components/admin-web-hook-event-chooser.js.es6 @@ -1,38 +1,40 @@ -import computed from 'ember-addons/ember-computed-decorators'; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNames: ['hook-event'], - typeName: Ember.computed.alias('type.name'), + classNames: ["hook-event"], + typeName: Ember.computed.alias("type.name"), - @computed('typeName') + @computed("typeName") name(typeName) { return I18n.t(`admin.web_hooks.${typeName}_event.name`); }, - @computed('typeName') + @computed("typeName") details(typeName) { return I18n.t(`admin.web_hooks.${typeName}_event.details`); }, - @computed('model.[]', 'typeName') + @computed("model.[]", "typeName") eventTypeExists(eventTypes, typeName) { return eventTypes.any(event => event.name === typeName); }, - @computed('eventTypeExists') + @computed("eventTypeExists") enabled: { get(eventTypeExists) { return eventTypeExists; }, set(value, eventTypeExists) { - const type = this.get('type'); - const model = this.get('model'); + const type = this.get("type"); + const model = this.get("model"); // add an association when not exists if (value !== eventTypeExists) { if (value) { model.addObject(type); } else { - model.removeObjects(model.filter(eventType => eventType.name === type.name)); + model.removeObjects( + model.filter(eventType => eventType.name === type.name) + ); } } diff --git a/app/assets/javascripts/admin/components/admin-web-hook-event.js.es6 b/app/assets/javascripts/admin/components/admin-web-hook-event.js.es6 index 264e827da6..5191412c29 100644 --- a/app/assets/javascripts/admin/components/admin-web-hook-event.js.es6 +++ b/app/assets/javascripts/admin/components/admin-web-hook-event.js.es6 @@ -1,78 +1,92 @@ -import computed from 'ember-addons/ember-computed-decorators'; -import { ajax } from 'discourse/lib/ajax'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import { ensureJSON, plainJSON, prettyJSON } from 'discourse/lib/formatter'; +import computed from "ember-addons/ember-computed-decorators"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { ensureJSON, plainJSON, prettyJSON } from "discourse/lib/formatter"; export default Ember.Component.extend({ - tagName: 'li', + tagName: "li", expandDetails: null, - @computed('model.status') + @computed("model.status") statusColorClasses(status) { - if (!status) return ''; + if (!status) return ""; if (status >= 200 && status <= 299) { - return 'text-successful'; + return "text-successful"; } else { - return 'text-danger'; + return "text-danger"; } }, - @computed('model.created_at') + @computed("model.created_at") createdAt(createdAt) { - return moment(createdAt).format('YYYY-MM-DD HH:mm:ss'); + return moment(createdAt).format("YYYY-MM-DD HH:mm:ss"); }, - @computed('model.duration') + @computed("model.duration") completion(duration) { const seconds = Math.floor(duration / 10.0) / 100.0; - return I18n.t('admin.web_hooks.events.completed_in', { count: seconds }); + return I18n.t("admin.web_hooks.events.completed_in", { count: seconds }); }, actions: { redeliver() { - return bootbox.confirm(I18n.t('admin.web_hooks.events.redeliver_confirm'), I18n.t('no_value'), I18n.t('yes_value'), result => { - if (result) { - ajax(`/admin/api/web_hooks/${this.get('model.web_hook_id')}/events/${this.get('model.id')}/redeliver`, { type: 'POST' }).then(json => { - this.set('model', json.web_hook_event); - }).catch(popupAjaxError); + return bootbox.confirm( + I18n.t("admin.web_hooks.events.redeliver_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + ajax( + `/admin/api/web_hooks/${this.get( + "model.web_hook_id" + )}/events/${this.get("model.id")}/redeliver`, + { type: "POST" } + ) + .then(json => { + this.set("model", json.web_hook_event); + }) + .catch(popupAjaxError); + } } - }); + ); }, toggleRequest() { - const expandDetailsKey = 'request'; + const expandDetailsKey = "request"; - if (this.get('expandDetails') !== expandDetailsKey) { - let headers = _.extend({ - 'Request URL': this.get('model.request_url'), - 'Request method': 'POST' - }, ensureJSON(this.get('model.headers'))); + if (this.get("expandDetails") !== expandDetailsKey) { + let headers = _.extend( + { + "Request URL": this.get("model.request_url"), + "Request method": "POST" + }, + ensureJSON(this.get("model.headers")) + ); this.setProperties({ headers: plainJSON(headers), - body: prettyJSON(this.get('model.payload')), + body: prettyJSON(this.get("model.payload")), expandDetails: expandDetailsKey, - bodyLabel: I18n.t('admin.web_hooks.events.payload') + bodyLabel: I18n.t("admin.web_hooks.events.payload") }); } else { - this.set('expandDetails', null); + this.set("expandDetails", null); } }, toggleResponse() { - const expandDetailsKey = 'response'; + const expandDetailsKey = "response"; - if (this.get('expandDetails') !== expandDetailsKey) { + if (this.get("expandDetails") !== expandDetailsKey) { this.setProperties({ - headers: plainJSON(this.get('model.response_headers')), - body: this.get('model.response_body'), + headers: plainJSON(this.get("model.response_headers")), + body: this.get("model.response_body"), expandDetails: expandDetailsKey, - bodyLabel: I18n.t('admin.web_hooks.events.body') + bodyLabel: I18n.t("admin.web_hooks.events.body") }); } else { - this.set('expandDetails', null); + this.set("expandDetails", null); } } } - }); diff --git a/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 b/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 index c5f1a4a244..9e508e904c 100644 --- a/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 +++ b/app/assets/javascripts/admin/components/admin-web-hook-status.js.es6 @@ -1,28 +1,32 @@ -import computed from 'ember-addons/ember-computed-decorators'; -import { iconHTML } from 'discourse-common/lib/icon-library'; -import { bufferedRender } from 'discourse-common/lib/buffered-render'; +import computed from "ember-addons/ember-computed-decorators"; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { bufferedRender } from "discourse-common/lib/buffered-render"; -export default Ember.Component.extend(bufferedRender({ - classes: ["text-muted", "text-danger", "text-successful"], - icons: ["circle-o", "times-circle", "circle"], +export default Ember.Component.extend( + bufferedRender({ + classes: ["text-muted", "text-danger", "text-successful"], + icons: ["circle-o", "times-circle", "circle"], - @computed('deliveryStatuses', 'model.last_delivery_status') - status(deliveryStatuses, lastDeliveryStatus) { - return deliveryStatuses.find(s => s.id === lastDeliveryStatus); - }, + @computed("deliveryStatuses", "model.last_delivery_status") + status(deliveryStatuses, lastDeliveryStatus) { + return deliveryStatuses.find(s => s.id === lastDeliveryStatus); + }, - @computed('status.id', 'icons') - icon(statusId, icons) { - return icons[statusId - 1]; - }, + @computed("status.id", "icons") + icon(statusId, icons) { + return icons[statusId - 1]; + }, - @computed('status.id', 'classes') - class(statusId, classes) { - return classes[statusId - 1]; - }, + @computed("status.id", "classes") + class(statusId, classes) { + return classes[statusId - 1]; + }, - buildBuffer(buffer) { - buffer.push(iconHTML(this.get('icon'), { class: this.get('class') })); - buffer.push(I18n.t(`admin.web_hooks.delivery_status.${this.get('status.name')}`)); - } -})); + buildBuffer(buffer) { + buffer.push(iconHTML(this.get("icon"), { class: this.get("class") })); + buffer.push( + I18n.t(`admin.web_hooks.delivery_status.${this.get("status.name")}`) + ); + } + }) +); diff --git a/app/assets/javascripts/admin/components/admin-wrapper.js.es6 b/app/assets/javascripts/admin/components/admin-wrapper.js.es6 index 118728f66c..034e2a0af4 100644 --- a/app/assets/javascripts/admin/components/admin-wrapper.js.es6 +++ b/app/assets/javascripts/admin/components/admin-wrapper.js.es6 @@ -1,11 +1,11 @@ export default Ember.Component.extend({ didInsertElement() { this._super(); - $('body').addClass('admin-interface'); + $("body").addClass("admin-interface"); }, willDestroyElement() { this._super(); - $('body').removeClass('admin-interface'); + $("body").removeClass("admin-interface"); } }); diff --git a/app/assets/javascripts/admin/components/cancel-link.js.es6 b/app/assets/javascripts/admin/components/cancel-link.js.es6 index 9250c1ae73..91ad923ffc 100644 --- a/app/assets/javascripts/admin/components/cancel-link.js.es6 +++ b/app/assets/javascripts/admin/components/cancel-link.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: '' + tagName: "" }); diff --git a/app/assets/javascripts/admin/components/color-input.js.es6 b/app/assets/javascripts/admin/components/color-input.js.es6 index 005c4f5d4b..17ae5a9296 100644 --- a/app/assets/javascripts/admin/components/color-input.js.es6 +++ b/app/assets/javascripts/admin/components/color-input.js.es6 @@ -1,4 +1,4 @@ -import {default as loadScript, loadCSS } from 'discourse/lib/load-script'; +import { default as loadScript, loadCSS } from "discourse/lib/load-script"; /** An input field for a color. @@ -8,35 +8,43 @@ import {default as loadScript, loadCSS } from 'discourse/lib/load-script'; @params valid is a boolean indicating if the input field is a valid color. **/ export default Ember.Component.extend({ - classNames: ['color-picker'], + classNames: ["color-picker"], hexValueChanged: function() { - var hex = this.get('hexValue'); - let $text = this.$('input.hex-input'); + var hex = this.get("hexValue"); + let $text = this.$("input.hex-input"); - if (this.get('valid')) { - $text.attr('style', 'color: ' + (this.get('brightnessValue') > 125 ? 'black' : 'white') + '; background-color: #' + hex + ';'); + if (this.get("valid")) { + $text.attr( + "style", + "color: " + + (this.get("brightnessValue") > 125 ? "black" : "white") + + "; background-color: #" + + hex + + ";" + ); - if (this.get('pickerLoaded')) { - this.$('.picker').spectrum({color: "#" + this.get('hexValue')}); + if (this.get("pickerLoaded")) { + this.$(".picker").spectrum({ color: "#" + this.get("hexValue") }); } } else { - $text.attr('style', ''); + $text.attr("style", ""); } - }.observes('hexValue', 'brightnessValue', 'valid'), + }.observes("hexValue", "brightnessValue", "valid"), didInsertElement() { - loadScript('/javascripts/spectrum.js').then(()=>{ - loadCSS('/javascripts/spectrum.css').then(()=>{ - Em.run.schedule('afterRender', ()=>{ - this.$('.picker').spectrum({color: "#" + this.get('hexValue')}) - .on("change.spectrum", (me, color)=>{ - this.set('hexValue', color.toHexString().replace("#","")); - }); - this.set('pickerLoaded', true); + loadScript("/javascripts/spectrum.js").then(() => { + loadCSS("/javascripts/spectrum.css").then(() => { + Em.run.schedule("afterRender", () => { + this.$(".picker") + .spectrum({ color: "#" + this.get("hexValue") }) + .on("change.spectrum", (me, color) => { + this.set("hexValue", color.toHexString().replace("#", "")); + }); + this.set("pickerLoaded", true); }); }); }); - Em.run.schedule('afterRender', ()=>{ + Em.run.schedule("afterRender", () => { this.hexValueChanged(); }); } diff --git a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 index 8fae065f7b..0435f8237e 100644 --- a/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-inline-table.js.es6 @@ -9,11 +9,12 @@ export default Ember.Component.extend(AsyncReport, { let payload = this.buildPayload(["total", "prev30Days"]); - return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => { - return ajax(dataSource, payload) - .then(response => { + return Ember.RSVP.Promise.all( + this.get("dataSources").map(dataSource => { + return ajax(dataSource, payload).then(response => { this.get("reports").pushObject(this.loadReport(response.report)); }); - })); + }) + ); } }); diff --git a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 index 5992e47953..1f0d4b7cba 100644 --- a/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-mini-chart.js.es6 @@ -1,7 +1,7 @@ import { ajax } from "discourse/lib/ajax"; import AsyncReport from "admin/mixins/async-report"; import Report from "admin/models/report"; -import { number } from 'discourse/lib/formatter'; +import { number } from "discourse/lib/formatter"; import loadScript from "discourse/lib/load-script"; import { registerTooltip, unregisterTooltip } from "discourse/lib/tooltip"; @@ -9,9 +9,8 @@ function collapseWeekly(data, average) { let aggregate = []; let bucket, i; let offset = data.length % 7; - for(i = offset; i < data.length; i++) { - - if (bucket && (i % 7 === offset)) { + for (i = offset; i < data.length; i++) { + if (bucket && i % 7 === offset) { if (average) { bucket.y = parseFloat((bucket.y / 7.0).toFixed(2)); } @@ -59,12 +58,13 @@ export default Ember.Component.extend(AsyncReport, { this._chart = null; } - return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => { - return ajax(dataSource, payload) - .then(response => { + return Ember.RSVP.Promise.all( + this.get("dataSources").map(dataSource => { + return ajax(dataSource, payload).then(response => { this.get("reports").pushObject(this.loadReport(response.report)); }); - })); + }) + ); }, loadReport(report, previousReport) { @@ -77,7 +77,9 @@ export default Ember.Component.extend(AsyncReport, { if (previousReport && previousReport.color.length) { report.color = previousReport.color; } else { - const dataSourceNameIndex = this.get("dataSourceNames").split(",").indexOf(report.type); + const dataSourceNameIndex = this.get("dataSourceNames") + .split(",") + .indexOf(report.type); report.color = this.pickColorAtIndex(dataSourceNameIndex); } @@ -94,13 +96,17 @@ export default Ember.Component.extend(AsyncReport, { const reportsForPeriod = this.get("reportsForPeriod"); - const labels = Ember.makeArray(reportsForPeriod.get("firstObject.data")).map(d => d.x); + const labels = Ember.makeArray( + reportsForPeriod.get("firstObject.data") + ).map(d => d.x); const data = { labels, datasets: reportsForPeriod.map(report => { return { - data: Ember.makeArray(report.data).map(d => number(d.y, { ceil: true })), + data: Ember.makeArray(report.data).map(d => + Math.round(parseFloat(d.y)) + ), backgroundColor: "rgba(200,220,240,0.3)", borderColor: report.color }; @@ -116,6 +122,7 @@ export default Ember.Component.extend(AsyncReport, { if (this._chart) { this._chart.destroy(); } + this._chart = new window.Chart(context, this._buildChartConfig(data)); }); }); @@ -128,7 +135,8 @@ export default Ember.Component.extend(AsyncReport, { options: { tooltips: { callbacks: { - title: (context) => moment(context[0].xLabel, "YYYY-MM-DD").format("LL") + title: context => + moment(context[0].xLabel, "YYYY-MM-DD").format("LL") } }, legend: { @@ -145,20 +153,24 @@ export default Ember.Component.extend(AsyncReport, { } }, scales: { - yAxes: [{ - display: true, - ticks: { callback: (label) => number(label, { ceil: true }) } - }], - xAxes: [{ - display: true, - gridLines: { display: false }, - type: "time", - time: { - parser: "YYYY-MM-DD" + yAxes: [ + { + display: true, + ticks: { callback: label => number(label) } } - }], + ], + xAxes: [ + { + display: true, + gridLines: { display: false }, + type: "time", + time: { + parser: "YYYY-MM-DD" + } + } + ] } - }, + } }; } }); diff --git a/app/assets/javascripts/admin/components/dashboard-table.js.es6 b/app/assets/javascripts/admin/components/dashboard-table.js.es6 index 96b74e6e15..fad090efac 100644 --- a/app/assets/javascripts/admin/components/dashboard-table.js.es6 +++ b/app/assets/javascripts/admin/components/dashboard-table.js.es6 @@ -9,11 +9,12 @@ export default Ember.Component.extend(AsyncReport, { let payload = this.buildPayload(["total", "prev30Days"]); - return Ember.RSVP.Promise.all(this.get("dataSources").map(dataSource => { - return ajax(dataSource, payload) - .then(response => { + return Ember.RSVP.Promise.all( + this.get("dataSources").map(dataSource => { + return ajax(dataSource, payload).then(response => { this.get("reports").pushObject(this.loadReport(response.report)); }); - })); + }) + ); } }); diff --git a/app/assets/javascripts/admin/components/embeddable-host.js.es6 b/app/assets/javascripts/admin/components/embeddable-host.js.es6 index 70c1ed272c..7264d42c1c 100644 --- a/app/assets/javascripts/admin/components/embeddable-host.js.es6 +++ b/app/assets/javascripts/admin/components/embeddable-host.js.es6 @@ -1,63 +1,79 @@ -import { bufferedProperty } from 'discourse/mixins/buffered-content'; -import computed from 'ember-addons/ember-computed-decorators'; -import { on, observes } from 'ember-addons/ember-computed-decorators'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; +import { bufferedProperty } from "discourse/mixins/buffered-content"; +import computed from "ember-addons/ember-computed-decorators"; +import { on, observes } from "ember-addons/ember-computed-decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; -export default Ember.Component.extend(bufferedProperty('host'), { +export default Ember.Component.extend(bufferedProperty("host"), { editToggled: false, - tagName: 'tr', + tagName: "tr", categoryId: null, - editing: Ember.computed.or('host.isNew', 'editToggled'), + editing: Ember.computed.or("host.isNew", "editToggled"), - @on('didInsertElement') - @observes('editing') + @on("didInsertElement") + @observes("editing") _focusOnInput() { - Ember.run.schedule('afterRender', () => { this.$('.host-name').focus(); }); + Ember.run.schedule("afterRender", () => { + this.$(".host-name").focus(); + }); }, - @computed('buffered.host', 'host.isSaving') + @computed("buffered.host", "host.isSaving") cantSave(host, isSaving) { return isSaving || Ember.isEmpty(host); }, actions: { edit() { - this.set('categoryId', this.get('host.category.id')); - this.set('editToggled', true); + this.set("categoryId", this.get("host.category.id")); + this.set("editToggled", true); }, save() { - if (this.get('cantSave')) { return; } + if (this.get("cantSave")) { + return; + } - const props = this.get('buffered').getProperties('host', 'path_whitelist', 'class_name'); - props.category_id = this.get('categoryId'); + const props = this.get("buffered").getProperties( + "host", + "path_whitelist", + "class_name" + ); + props.category_id = this.get("categoryId"); - const host = this.get('host'); + const host = this.get("host"); - host.save(props).then(() => { - host.set('category', Discourse.Category.findById(this.get('categoryId'))); - this.set('editToggled', false); - }).catch(popupAjaxError); + host + .save(props) + .then(() => { + host.set( + "category", + Discourse.Category.findById(this.get("categoryId")) + ); + this.set("editToggled", false); + }) + .catch(popupAjaxError); }, delete() { - bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => { + bootbox.confirm(I18n.t("admin.embedding.confirm_delete"), result => { if (result) { - this.get('host').destroyRecord().then(() => { - this.sendAction('deleteHost', this.get('host')); - }); + this.get("host") + .destroyRecord() + .then(() => { + this.sendAction("deleteHost", this.get("host")); + }); } }); }, cancel() { - const host = this.get('host'); - if (host.get('isNew')) { - this.sendAction('deleteHost', host); + const host = this.get("host"); + if (host.get("isNew")) { + this.sendAction("deleteHost", host); } else { this.rollbackBuffer(); - this.set('editToggled', false); + this.set("editToggled", false); } } } diff --git a/app/assets/javascripts/admin/components/embedding-setting.js.es6 b/app/assets/javascripts/admin/components/embedding-setting.js.es6 index f1d31fcf68..4791e84e35 100644 --- a/app/assets/javascripts/admin/components/embedding-setting.js.es6 +++ b/app/assets/javascripts/admin/components/embedding-setting.js.es6 @@ -1,22 +1,30 @@ -import computed from 'ember-addons/ember-computed-decorators'; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNames: ['embed-setting'], + classNames: ["embed-setting"], - @computed('field') - inputId(field) { return field.dasherize(); }, + @computed("field") + inputId(field) { + return field.dasherize(); + }, - @computed('field') - translationKey(field) { return `admin.embedding.${field}`; }, + @computed("field") + translationKey(field) { + return `admin.embedding.${field}`; + }, - @computed('type') - isCheckbox(type) { return type === "checkbox"; }, + @computed("type") + isCheckbox(type) { + return type === "checkbox"; + }, - @computed('value') + @computed("value") checked: { - get(value) { return !!value; }, + get(value) { + return !!value; + }, set(value) { - this.set('value', value); + this.set("value", value); return value; } } diff --git a/app/assets/javascripts/admin/components/flag-user-lists.js.es6 b/app/assets/javascripts/admin/components/flag-user-lists.js.es6 index cd843ff7ab..ae6094c6a7 100644 --- a/app/assets/javascripts/admin/components/flag-user-lists.js.es6 +++ b/app/assets/javascripts/admin/components/flag-user-lists.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - classNames: ['flag-user-lists'] + classNames: ["flag-user-lists"] }); diff --git a/app/assets/javascripts/admin/components/flagged-post-response.js.es6 b/app/assets/javascripts/admin/components/flagged-post-response.js.es6 index e031f33e03..e8dc2a230c 100644 --- a/app/assets/javascripts/admin/components/flagged-post-response.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post-response.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - classNames: ['flagged-post-response'] + classNames: ["flagged-post-response"] }); diff --git a/app/assets/javascripts/admin/components/flagged-post-title.js.es6 b/app/assets/javascripts/admin/components/flagged-post-title.js.es6 index 7c1c013375..4a6fa5d604 100644 --- a/app/assets/javascripts/admin/components/flagged-post-title.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post-title.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: 'h3' + tagName: "h3" }); diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index 8aff3d2a2d..03edebab6c 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -1,21 +1,21 @@ -import showModal from 'discourse/lib/show-modal'; -import computed from 'ember-addons/ember-computed-decorators'; +import showModal from "discourse/lib/show-modal"; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ adminTools: Ember.inject.service(), expanded: false, - tagName: 'div', + tagName: "div", classNameBindings: [ - ':flagged-post', - 'flaggedPost.hidden:hidden-post', - 'flaggedPost.deleted' + ":flagged-post", + "flaggedPost.hidden:hidden-post", + "flaggedPost.deleted" ], - canAct: Ember.computed.alias('actableFilter'), + canAct: Ember.computed.alias("actableFilter"), - @computed('filter') + @computed("filter") actableFilter(filter) { - return filter === 'active'; + return filter === "active"; }, removeAfter(promise) { @@ -24,7 +24,7 @@ export default Ember.Component.extend({ _spawnModal(name, model, modalClass) { let controller = showModal(name, { model, admin: true, modalClass }); - controller.removeAfter = (p) => this.removeAfter(p); + controller.removeAfter = p => this.removeAfter(p); }, actions: { @@ -33,23 +33,25 @@ export default Ember.Component.extend({ }, disagree() { - this.removeAfter(this.get('flaggedPost').disagreeFlags()); + this.removeAfter(this.get("flaggedPost").disagreeFlags()); }, defer() { - this.removeAfter(this.get('flaggedPost').deferFlags()); + this.removeAfter(this.get("flaggedPost").deferFlags()); }, expand() { - this.get('flaggedPost').expandHidden().then(() => { - this.set('expanded', true); - }); + this.get("flaggedPost") + .expandHidden() + .then(() => { + this.set("expanded", true); + }); }, showModerationHistory() { - this.get('adminTools').showModerationHistory({ - filter: 'post', - post_id: this.get('flaggedPost.id') + this.get("adminTools").showModerationHistory({ + filter: "post", + post_id: this.get("flaggedPost.id") }); } } diff --git a/app/assets/javascripts/admin/components/highlighted-code.js.es6 b/app/assets/javascripts/admin/components/highlighted-code.js.es6 index 4fc413fd89..62cf58f21b 100644 --- a/app/assets/javascripts/admin/components/highlighted-code.js.es6 +++ b/app/assets/javascripts/admin/components/highlighted-code.js.es6 @@ -1,12 +1,10 @@ -import { on, observes } from 'ember-addons/ember-computed-decorators'; -import highlightSyntax from 'discourse/lib/highlight-syntax'; +import { on, observes } from "ember-addons/ember-computed-decorators"; +import highlightSyntax from "discourse/lib/highlight-syntax"; export default Ember.Component.extend({ - - @on('didInsertElement') - @observes('code') + @on("didInsertElement") + @observes("code") _refresh: function() { highlightSyntax(this.$()); } - }); diff --git a/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 b/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 index 5c168760c7..f14ba62201 100644 --- a/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 +++ b/app/assets/javascripts/admin/components/inline-edit-checkbox.js.es6 @@ -1,12 +1,15 @@ -import {default as computed, observes} from "ember-addons/ember-computed-decorators"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - init(){ + init() { this._super(); this.set("checkedInternal", this.get("checked")); }, - classNames: ['inline-edit'], + classNames: ["inline-edit"], @observes("checked") checkedChanged() { @@ -20,15 +23,15 @@ export default Ember.Component.extend({ @computed("checked", "checkedInternal") changed(checked, checkedInternal) { - return (!!checked) !== (!!checkedInternal); + return !!checked !== !!checkedInternal; }, actions: { - cancelled(){ + cancelled() { this.set("checkedInternal", this.get("checked")); }, - finished(){ + finished() { this.set("checked", this.get("checkedInternal")); this.sendAction(); } diff --git a/app/assets/javascripts/admin/components/ip-lookup.js.es6 b/app/assets/javascripts/admin/components/ip-lookup.js.es6 index a185e4c0b6..3f06ba38d6 100644 --- a/app/assets/javascripts/admin/components/ip-lookup.js.es6 +++ b/app/assets/javascripts/admin/components/ip-lookup.js.es6 @@ -1,15 +1,17 @@ -import { ajax } from 'discourse/lib/ajax'; -import AdminUser from 'admin/models/admin-user'; +import { ajax } from "discourse/lib/ajax"; +import AdminUser from "admin/models/admin-user"; export default Ember.Component.extend({ classNames: ["ip-lookup"], - city: function () { + city: function() { return [ this.get("location.city"), this.get("location.region"), this.get("location.country") - ].filter(Boolean).join(", "); + ] + .filter(Boolean) + .join(", "); }.property("location.{city,region,country}"), otherAccountsToDelete: function() { @@ -20,14 +22,14 @@ export default Ember.Component.extend({ }.property("other_accounts", "totalOthersWithSameIP"), actions: { - lookup: function () { + lookup: function() { var self = this; this.set("show", true); if (!this.get("location")) { ajax("/admin/users/ip-info", { data: { ip: this.get("ip") } - }).then(function (location) { + }).then(function(location) { self.set("location", Em.Object.create(location)); }); } @@ -36,50 +38,57 @@ export default Ember.Component.extend({ this.set("otherAccountsLoading", true); var data = { - "ip": this.get("ip"), - "exclude": this.get("userId"), - "order": "trust_level DESC" + ip: this.get("ip"), + exclude: this.get("userId"), + order: "trust_level DESC" }; - ajax("/admin/users/total-others-with-same-ip", { data }).then(function (result) { + ajax("/admin/users/total-others-with-same-ip", { data }).then(function( + result + ) { self.set("totalOthersWithSameIP", result.total); }); - AdminUser.findAll("active", data).then(function (users) { + AdminUser.findAll("active", data).then(function(users) { self.setProperties({ other_accounts: users, - otherAccountsLoading: false, + otherAccountsLoading: false }); }); } }, - hide: function () { + hide: function() { this.set("show", false); }, deleteOtherAccounts: function() { var self = this; - bootbox.confirm(I18n.t("ip_lookup.confirm_delete_other_accounts"), I18n.t("no_value"), I18n.t("yes_value"), function (confirmed) { - if (confirmed) { - self.setProperties({ - other_accounts: null, - otherAccountsLoading: true, - totalOthersWithSameIP: null - }); + bootbox.confirm( + I18n.t("ip_lookup.confirm_delete_other_accounts"), + I18n.t("no_value"), + I18n.t("yes_value"), + function(confirmed) { + if (confirmed) { + self.setProperties({ + other_accounts: null, + otherAccountsLoading: true, + totalOthersWithSameIP: null + }); - ajax("/admin/users/delete-others-with-same-ip.json", { - type: "DELETE", - data: { - "ip": self.get("ip"), - "exclude": self.get("userId"), - "order": "trust_level DESC" - } - }).then(function() { - self.send("lookup"); - }); + ajax("/admin/users/delete-others-with-same-ip.json", { + type: "DELETE", + data: { + ip: self.get("ip"), + exclude: self.get("userId"), + order: "trust_level DESC" + } + }).then(function() { + self.send("lookup"); + }); + } } - }); + ); } } }); diff --git a/app/assets/javascripts/admin/components/moderation-history-item.js.es6 b/app/assets/javascripts/admin/components/moderation-history-item.js.es6 index b8674a8aaf..b7620b66cd 100644 --- a/app/assets/javascripts/admin/components/moderation-history-item.js.es6 +++ b/app/assets/javascripts/admin/components/moderation-history-item.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: 'tr', + tagName: "tr" }); diff --git a/app/assets/javascripts/admin/components/penalty-post-action.js.es6 b/app/assets/javascripts/admin/components/penalty-post-action.js.es6 index d89c69a32d..b4d82aba8b 100644 --- a/app/assets/javascripts/admin/components/penalty-post-action.js.es6 +++ b/app/assets/javascripts/admin/components/penalty-post-action.js.es6 @@ -1,6 +1,6 @@ -import computed from 'ember-addons/ember-computed-decorators'; +import computed from "ember-addons/ember-computed-decorators"; -const ACTIONS = ['delete', 'edit', 'none']; +const ACTIONS = ["delete", "edit", "none"]; export default Ember.Component.extend({ postAction: null, postEdit: null, @@ -12,19 +12,19 @@ export default Ember.Component.extend({ }); }, - editing: Ember.computed.equal('postAction', 'edit'), + editing: Ember.computed.equal("postAction", "edit"), actions: { penaltyChanged() { - let postAction = this.get('postAction'); + let postAction = this.get("postAction"); // If we switch to edit mode, jump to the edit textarea - if (postAction === 'edit') { - Ember.run.scheduleOnce('afterRender', () => { + if (postAction === "edit") { + Ember.run.scheduleOnce("afterRender", () => { let $elem = this.$(); - let body = $elem.closest('.modal-body'); + let body = $elem.closest(".modal-body"); body.scrollTop(body.height()); - $elem.find('.post-editor').focus(); + $elem.find(".post-editor").focus(); }); } } diff --git a/app/assets/javascripts/admin/components/permalink-form.js.es6 b/app/assets/javascripts/admin/components/permalink-form.js.es6 index 90dcf2a7b2..969840a308 100644 --- a/app/assets/javascripts/admin/components/permalink-form.js.es6 +++ b/app/assets/javascripts/admin/components/permalink-form.js.es6 @@ -1,45 +1,58 @@ -import Permalink from 'admin/models/permalink'; +import Permalink from "admin/models/permalink"; export default Ember.Component.extend({ - classNames: ['permalink-form'], + classNames: ["permalink-form"], formSubmitted: false, - permalinkType: 'topic_id', + permalinkType: "topic_id", permalinkTypes: function() { return [ - {id: 'topic_id', name: I18n.t('admin.permalink.topic_id')}, - {id: 'post_id', name: I18n.t('admin.permalink.post_id')}, - {id: 'category_id', name: I18n.t('admin.permalink.category_id')}, - {id: 'external_url', name: I18n.t('admin.permalink.external_url')} + { id: "topic_id", name: I18n.t("admin.permalink.topic_id") }, + { id: "post_id", name: I18n.t("admin.permalink.post_id") }, + { id: "category_id", name: I18n.t("admin.permalink.category_id") }, + { id: "external_url", name: I18n.t("admin.permalink.external_url") } ]; }.property(), permalinkTypePlaceholder: function() { - return 'admin.permalink.' + this.get('permalinkType'); - }.property('permalinkType'), + return "admin.permalink." + this.get("permalinkType"); + }.property("permalinkType"), actions: { submit: function() { - if (!this.get('formSubmitted')) { + if (!this.get("formSubmitted")) { const self = this; - self.set('formSubmitted', true); - const permalink = Permalink.create({url: self.get('url'), permalink_type: self.get('permalinkType'), permalink_type_value: self.get('permalink_type_value')}); - permalink.save().then(function(result) { - self.set('url', ''); - self.set('permalink_type_value', ''); - self.set('formSubmitted', false); - self.sendAction('action', Permalink.create(result.permalink)); - Em.run.schedule('afterRender', function() { self.$('.permalink-url').focus(); }); - }, function(e) { - self.set('formSubmitted', false); - let error; - if (e.responseJSON && e.responseJSON.errors) { - error = I18n.t("generic_error_with_reason", {error: e.responseJSON.errors.join('. ')}); - } else { - error = I18n.t("generic_error"); - } - bootbox.alert(error, function() { self.$('.permalink-url').focus(); }); + self.set("formSubmitted", true); + const permalink = Permalink.create({ + url: self.get("url"), + permalink_type: self.get("permalinkType"), + permalink_type_value: self.get("permalink_type_value") }); + permalink.save().then( + function(result) { + self.set("url", ""); + self.set("permalink_type_value", ""); + self.set("formSubmitted", false); + self.sendAction("action", Permalink.create(result.permalink)); + Em.run.schedule("afterRender", function() { + self.$(".permalink-url").focus(); + }); + }, + function(e) { + self.set("formSubmitted", false); + let error; + if (e.responseJSON && e.responseJSON.errors) { + error = I18n.t("generic_error_with_reason", { + error: e.responseJSON.errors.join(". ") + }); + } else { + error = I18n.t("generic_error"); + } + bootbox.alert(error, function() { + self.$(".permalink-url").focus(); + }); + } + ); } } }, @@ -47,10 +60,11 @@ export default Ember.Component.extend({ didInsertElement: function() { var self = this; self._super(); - Em.run.schedule('afterRender', function() { - self.$('.external-url').keydown(function(e) { - if (e.keyCode === 13) { // enter key - self.send('submit'); + Em.run.schedule("afterRender", function() { + self.$(".external-url").keydown(function(e) { + if (e.keyCode === 13) { + // enter key + self.send("submit"); } }); }); diff --git a/app/assets/javascripts/admin/components/resumable-upload.js.es6 b/app/assets/javascripts/admin/components/resumable-upload.js.es6 index 36f420d100..4a8545b3ab 100644 --- a/app/assets/javascripts/admin/components/resumable-upload.js.es6 +++ b/app/assets/javascripts/admin/components/resumable-upload.js.es6 @@ -1,5 +1,5 @@ -import { iconHTML } from 'discourse-common/lib/icon-library'; -import { bufferedRender } from 'discourse-common/lib/buffered-render'; +import { iconHTML } from "discourse-common/lib/icon-library"; +import { bufferedRender } from "discourse-common/lib/buffered-render"; /*global Resumable:true */ @@ -13,111 +13,119 @@ import { bufferedRender } from 'discourse-common/lib/buffered-render'; uploadText="UPLOAD" }} **/ -export default Ember.Component.extend(bufferedRender({ - tagName: "button", - classNames: ["btn", "ru"], - classNameBindings: ["isUploading"], - attributeBindings: ["translatedTitle:title"], +export default Ember.Component.extend( + bufferedRender({ + tagName: "button", + classNames: ["btn", "ru"], + classNameBindings: ["isUploading"], + attributeBindings: ["translatedTitle:title"], - resumable: null, + resumable: null, - isUploading: false, - progress: 0, + isUploading: false, + progress: 0, - rerenderTriggers: ['isUploading', 'progress'], + rerenderTriggers: ["isUploading", "progress"], - translatedTitle: function() { - const title = this.get('title'); - return title ? I18n.t(title) : this.get('text'); - }.property('title', 'text'), + translatedTitle: function() { + const title = this.get("title"); + return title ? I18n.t(title) : this.get("text"); + }.property("title", "text"), - text: function() { - if (this.get("isUploading")) { - return this.get("progress") + " %"; - } else { - return this.get("uploadText"); - } - }.property("isUploading", "progress"), + text: function() { + if (this.get("isUploading")) { + return this.get("progress") + " %"; + } else { + return this.get("uploadText"); + } + }.property("isUploading", "progress"), - buildBuffer(buffer) { - const icon = this.get("isUploading") ? "times" : "upload"; - buffer.push(iconHTML(icon)); - buffer.push("" + this.get("text") + ""); - buffer.push(""); - }, + buildBuffer(buffer) { + const icon = this.get("isUploading") ? "times" : "upload"; + buffer.push(iconHTML(icon)); + buffer.push("" + this.get("text") + ""); + buffer.push( + "" + ); + }, + + click: function() { + if (this.get("isUploading")) { + this.resumable.cancel(); + var self = this; + Em.run.later(function() { + self._reset(); + }); + return false; + } else { + return true; + } + }, + + _reset: function() { + this.setProperties({ isUploading: false, progress: 0 }); + }, + + _initialize: function() { + this.resumable = new Resumable({ + target: Discourse.getURL(this.get("target")), + maxFiles: 1, // only 1 file at a time + headers: { + "X-CSRF-Token": $("meta[name='csrf-token']").attr("content") + } + }); - click: function() { - if (this.get("isUploading")) { - this.resumable.cancel(); var self = this; - Em.run.later(function() { self._reset(); }); - return false; - } else { - return true; - } - }, - _reset: function() { - this.setProperties({ isUploading: false, progress: 0 }); - }, - - _initialize: function() { - this.resumable = new Resumable({ - target: Discourse.getURL(this.get("target")), - maxFiles: 1, // only 1 file at a time - headers: { "X-CSRF-Token": $("meta[name='csrf-token']").attr("content") } - }); - - var self = this; - - this.resumable.on("fileAdded", function() { - // automatically upload the selected file - self.resumable.upload(); - // mark as uploading - Em.run.later(function() { - self.set("isUploading", true); + this.resumable.on("fileAdded", function() { + // automatically upload the selected file + self.resumable.upload(); + // mark as uploading + Em.run.later(function() { + self.set("isUploading", true); + }); }); - }); - this.resumable.on("fileProgress", function(file) { - // update progress - Em.run.later(function() { - self.set("progress", parseInt(file.progress() * 100, 10)); + this.resumable.on("fileProgress", function(file) { + // update progress + Em.run.later(function() { + self.set("progress", parseInt(file.progress() * 100, 10)); + }); }); - }); - this.resumable.on("fileSuccess", function(file) { - Em.run.later(function() { - // mark as not uploading anymore - self._reset(); - // fire an event to allow the parent route to reload its model - self.sendAction("success", file.fileName); + this.resumable.on("fileSuccess", function(file) { + Em.run.later(function() { + // mark as not uploading anymore + self._reset(); + // fire an event to allow the parent route to reload its model + self.sendAction("success", file.fileName); + }); }); - }); - this.resumable.on("fileError", function(file, message) { - Em.run.later(function() { - // mark as not uploading anymore - self._reset(); - // fire an event to allow the parent route to display the error message - self.sendAction("error", file.fileName, message); + this.resumable.on("fileError", function(file, message) { + Em.run.later(function() { + // mark as not uploading anymore + self._reset(); + // fire an event to allow the parent route to display the error message + self.sendAction("error", file.fileName, message); + }); }); - }); + }.on("init"), - }.on("init"), + _assignBrowse: function() { + var self = this; + Em.run.schedule("afterRender", function() { + self.resumable.assignBrowse(self.$()); + }); + }.on("didInsertElement"), - _assignBrowse: function() { - var self = this; - Em.run.schedule("afterRender", function() { - self.resumable.assignBrowse(self.$()); - }); - }.on("didInsertElement"), - - _teardown: function() { - if (this.resumable) { - this.resumable.cancel(); - this.resumable = null; - } - }.on("willDestroyElement") - -})); + _teardown: function() { + if (this.resumable) { + this.resumable.cancel(); + this.resumable = null; + } + }.on("willDestroyElement") + }) +); diff --git a/app/assets/javascripts/admin/components/save-controls.js.es6 b/app/assets/javascripts/admin/components/save-controls.js.es6 index 414bbf3661..51adbaf3c5 100644 --- a/app/assets/javascripts/admin/components/save-controls.js.es6 +++ b/app/assets/javascripts/admin/components/save-controls.js.es6 @@ -1,13 +1,13 @@ -import computed from 'ember-addons/ember-computed-decorators'; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNames: ['controls'], + classNames: ["controls"], - buttonDisabled: Ember.computed.or('model.isSaving', 'saveDisabled'), + buttonDisabled: Ember.computed.or("model.isSaving", "saveDisabled"), - @computed('model.isSaving') + @computed("model.isSaving") savingText(saving) { - return saving ? 'saving' : 'save'; + return saving ? "saving" : "save"; }, actions: { diff --git a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 b/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 index ef6e7596d1..1a1dd97709 100644 --- a/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 +++ b/app/assets/javascripts/admin/components/screened-ip-address-form.js.es6 @@ -9,14 +9,14 @@ as an argument. **/ -import ScreenedIpAddress from 'admin/models/screened-ip-address'; -import computed from 'ember-addons/ember-computed-decorators'; -import { on } from 'ember-addons/ember-computed-decorators'; +import ScreenedIpAddress from "admin/models/screened-ip-address"; +import computed from "ember-addons/ember-computed-decorators"; +import { on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNames: ['screened-ip-address-form'], + classNames: ["screened-ip-address-form"], formSubmitted: false, - actionName: 'block', + actionName: "block", @computed adminWhitelistEnabled() { @@ -27,51 +27,71 @@ export default Ember.Component.extend({ actionNames(adminWhitelistEnabled) { if (adminWhitelistEnabled) { return [ - {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')}, - {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')}, - {id: 'allow_admin', name: I18n.t('admin.logs.screened_ips.actions.allow_admin')} + { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, + { + id: "do_nothing", + name: I18n.t("admin.logs.screened_ips.actions.do_nothing") + }, + { + id: "allow_admin", + name: I18n.t("admin.logs.screened_ips.actions.allow_admin") + } ]; } else { return [ - {id: 'block', name: I18n.t('admin.logs.screened_ips.actions.block')}, - {id: 'do_nothing', name: I18n.t('admin.logs.screened_ips.actions.do_nothing')} + { id: "block", name: I18n.t("admin.logs.screened_ips.actions.block") }, + { + id: "do_nothing", + name: I18n.t("admin.logs.screened_ips.actions.do_nothing") + } ]; } }, actions: { submit() { - if (!this.get('formSubmitted')) { - this.set('formSubmitted', true); + if (!this.get("formSubmitted")) { + this.set("formSubmitted", true); const screenedIpAddress = ScreenedIpAddress.create({ - ip_address: this.get('ip_address'), - action_name: this.get('actionName') - }); - screenedIpAddress.save().then(result => { - if (result.success) { - this.setProperties({ ip_address: '', formSubmitted: false }); - this.sendAction('action', ScreenedIpAddress.create(result.screened_ip_address)); - Ember.run.schedule('afterRender', () => this.$('.ip-address-input').focus()); - } else { - bootbox.alert(result.errors); - } - }).catch(e => { - this.set('formSubmitted', false); - const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ? - I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) : - I18n.t("generic_error"); - bootbox.alert(msg, () => this.$('.ip-address-input').focus()); + ip_address: this.get("ip_address"), + action_name: this.get("actionName") }); + screenedIpAddress + .save() + .then(result => { + if (result.success) { + this.setProperties({ ip_address: "", formSubmitted: false }); + this.sendAction( + "action", + ScreenedIpAddress.create(result.screened_ip_address) + ); + Ember.run.schedule("afterRender", () => + this.$(".ip-address-input").focus() + ); + } else { + bootbox.alert(result.errors); + } + }) + .catch(e => { + this.set("formSubmitted", false); + const msg = + e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors + ? I18n.t("generic_error_with_reason", { + error: e.jqXHR.responseJSON.errors.join(". ") + }) + : I18n.t("generic_error"); + bootbox.alert(msg, () => this.$(".ip-address-input").focus()); + }); } } }, @on("didInsertElement") _init() { - Ember.run.schedule('afterRender', () => { - this.$('.ip-address-input').keydown(e => { + Ember.run.schedule("afterRender", () => { + this.$(".ip-address-input").keydown(e => { if (e.keyCode === 13) { - this.send('submit'); + this.send("submit"); } }); }); diff --git a/app/assets/javascripts/admin/components/silence-details.js.es6 b/app/assets/javascripts/admin/components/silence-details.js.es6 index 9250c1ae73..91ad923ffc 100644 --- a/app/assets/javascripts/admin/components/silence-details.js.es6 +++ b/app/assets/javascripts/admin/components/silence-details.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: '' + tagName: "" }); diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index 98bdf950f6..bd68c7d11e 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -1,10 +1,10 @@ -import BufferedContent from 'discourse/mixins/buffered-content'; -import SiteSetting from 'admin/models/site-setting'; -import SettingComponent from 'admin/mixins/setting-component'; +import BufferedContent from "discourse/mixins/buffered-content"; +import SiteSetting from "admin/models/site-setting"; +import SettingComponent from "admin/mixins/setting-component"; export default Ember.Component.extend(BufferedContent, SettingComponent, { _save() { - const setting = this.get('buffered'); - return SiteSetting.update(setting.get('setting'), setting.get('value')); + const setting = this.get("buffered"); + return SiteSetting.update(setting.get("setting"), setting.get("value")); } }); diff --git a/app/assets/javascripts/admin/components/site-settings/bool.js.es6 b/app/assets/javascripts/admin/components/site-settings/bool.js.es6 index 6be1a14e27..f46e965832 100644 --- a/app/assets/javascripts/admin/components/site-settings/bool.js.es6 +++ b/app/assets/javascripts/admin/components/site-settings/bool.js.es6 @@ -1,17 +1,17 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("value") enabled: { get(value) { - if (Ember.isEmpty(value)) { return false; } + if (Ember.isEmpty(value)) { + return false; + } return value.toString() === "true"; }, set(value) { this.set("value", value ? "true" : "false"); return value; } - }, - + } }); diff --git a/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 index 487239b78f..36c712fa8d 100644 --- a/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 +++ b/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 @@ -1,7 +1,6 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - @computed("value") selectedCategories: { get(value) { @@ -12,5 +11,4 @@ export default Ember.Component.extend({ return value; } } - }); diff --git a/app/assets/javascripts/admin/components/site-text-summary.js.es6 b/app/assets/javascripts/admin/components/site-text-summary.js.es6 index 642164f871..0ebd139614 100644 --- a/app/assets/javascripts/admin/components/site-text-summary.js.es6 +++ b/app/assets/javascripts/admin/components/site-text-summary.js.es6 @@ -1,25 +1,27 @@ -import { on } from 'ember-addons/ember-computed-decorators'; +import { on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNames: ['site-text'], - classNameBindings: ['siteText.overridden'], + classNames: ["site-text"], + classNameBindings: ["siteText.overridden"], - @on('didInsertElement') + @on("didInsertElement") highlightTerm() { - const term = this.get('term'); + const term = this.get("term"); if (term) { - this.$('.site-text-id, .site-text-value').highlight(term, {className: 'text-highlight'}); + this.$(".site-text-id, .site-text-value").highlight(term, { + className: "text-highlight" + }); } - this.$('.site-text-value').ellipsis(); + this.$(".site-text-value").ellipsis(); }, click() { - this.send('edit'); + this.send("edit"); }, actions: { edit() { - this.sendAction('editAction', this.get('siteText')); + this.sendAction("editAction", this.get("siteText")); } } }); diff --git a/app/assets/javascripts/admin/components/staff-actions.js.es6 b/app/assets/javascripts/admin/components/staff-actions.js.es6 index 9e742526af..07764a8018 100644 --- a/app/assets/javascripts/admin/components/staff-actions.js.es6 +++ b/app/assets/javascripts/admin/components/staff-actions.js.es6 @@ -1,20 +1,20 @@ -import DiscourseURL from 'discourse/lib/url'; +import DiscourseURL from "discourse/lib/url"; export default Ember.Component.extend({ - classNames: ['table', 'staff-actions'], + classNames: ["table", "staff-actions"], willDestroyElement() { - this.$().off('click.discourse-staff-logs'); + this.$().off("click.discourse-staff-logs"); }, didInsertElement() { this._super(); - this.$().on('click.discourse-staff-logs', '[data-link-post-id]', e => { - let postId = $(e.target).attr('data-link-post-id'); + this.$().on("click.discourse-staff-logs", "[data-link-post-id]", e => { + let postId = $(e.target).attr("data-link-post-id"); - this.store.find('post', postId).then(p => { - DiscourseURL.routeTo(p.get('url')); + this.store.find("post", postId).then(p => { + DiscourseURL.routeTo(p.get("url")); }); return false; }); diff --git a/app/assets/javascripts/admin/components/suspension-details.js.es6 b/app/assets/javascripts/admin/components/suspension-details.js.es6 index 9250c1ae73..91ad923ffc 100644 --- a/app/assets/javascripts/admin/components/suspension-details.js.es6 +++ b/app/assets/javascripts/admin/components/suspension-details.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ - tagName: '' + tagName: "" }); diff --git a/app/assets/javascripts/admin/components/theme-setting.js.es6 b/app/assets/javascripts/admin/components/theme-setting.js.es6 index eb576e9b64..c95dd220db 100644 --- a/app/assets/javascripts/admin/components/theme-setting.js.es6 +++ b/app/assets/javascripts/admin/components/theme-setting.js.es6 @@ -1,9 +1,12 @@ -import BufferedContent from 'discourse/mixins/buffered-content'; -import SettingComponent from 'admin/mixins/setting-component'; +import BufferedContent from "discourse/mixins/buffered-content"; +import SettingComponent from "admin/mixins/setting-component"; export default Ember.Component.extend(BufferedContent, SettingComponent, { - layoutName: 'admin/templates/components/site-setting', + layoutName: "admin/templates/components/site-setting", _save() { - return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value')); + return this.get("model").saveSettings( + this.get("setting.setting"), + this.get("buffered.value") + ); } }); diff --git a/app/assets/javascripts/admin/components/value-list.js.es6 b/app/assets/javascripts/admin/components/value-list.js.es6 index 3aa66ff187..60e4a3cda7 100644 --- a/app/assets/javascripts/admin/components/value-list.js.es6 +++ b/app/assets/javascripts/admin/components/value-list.js.es6 @@ -1,5 +1,5 @@ export default Ember.Component.extend({ - classNameBindings: [':value-list'], + classNameBindings: [":value-list"], _enableSorting: function() { const self = this; @@ -10,16 +10,16 @@ export default Ember.Component.extend({ let over = null; let nodePlacement; - this.$().on('dragstart.discourse', '.values .value', function(e) { + this.$().on("dragstart.discourse", ".values .value", function(e) { dragging = e.currentTarget; - e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/html", e.currentTarget); }); - this.$().on('dragend.discourse', '.values .value', function() { + this.$().on("dragend.discourse", ".values .value", function() { Ember.run(function() { dragging.parentNode.removeChild(placeholder); - dragging.style.display = 'block'; + dragging.style.display = "block"; // Update data const from = Number(dragging.dataset.index); @@ -27,7 +27,7 @@ export default Ember.Component.extend({ if (from < to) to--; if (nodePlacement === "after") to++; - const collection = self.get('collection'); + const collection = self.get("collection"); const fromObj = collection.objectAt(from); collection.replace(from, 1); collection.replace(to, 0, [fromObj]); @@ -36,10 +36,12 @@ export default Ember.Component.extend({ return false; }); - this.$().on('dragover.discourse', '.values', function(e) { + this.$().on("dragover.discourse", ".values", function(e) { e.preventDefault(); - dragging.style.display = 'none'; - if (e.target.className === "placeholder") { return; } + dragging.style.display = "none"; + if (e.target.className === "placeholder") { + return; + } over = e.target; const relY = e.originalEvent.clientY - over.offsetTop; @@ -49,54 +51,61 @@ export default Ember.Component.extend({ if (relY > height) { nodePlacement = "after"; parent.insertBefore(placeholder, e.target.nextElementSibling); - } else if(relY < height) { + } else if (relY < height) { nodePlacement = "before"; parent.insertBefore(placeholder, e.target); } }); - }.on('didInsertElement'), + }.on("didInsertElement"), _removeSorting: function() { - this.$().off('dragover.discourse').off('dragend.discourse').off('dragstart.discourse'); - }.on('willDestroyElement'), + this.$() + .off("dragover.discourse") + .off("dragend.discourse") + .off("dragstart.discourse"); + }.on("willDestroyElement"), _setupCollection: function() { - const values = this.get('values'); - if (this.get('inputType') === "array") { - this.set('collection', values || []); + const values = this.get("values"); + if (this.get("inputType") === "array") { + this.set("collection", values || []); } else { - this.set('collection', (values && values.length) ? values.split("\n") : []); + this.set("collection", values && values.length ? values.split("\n") : []); } - }.on('init').observes('values'), + } + .on("init") + .observes("values"), _saveValues: function() { - if (this.get('inputType') === "array") { - this.set('values', this.get('collection')); + if (this.get("inputType") === "array") { + this.set("values", this.get("collection")); } else { - this.set('values', this.get('collection').join("\n")); + this.set("values", this.get("collection").join("\n")); } }, - inputInvalid: Ember.computed.empty('newValue'), + inputInvalid: Ember.computed.empty("newValue"), keyDown(e) { if (e.keyCode === 13) { - this.send('addValue'); + this.send("addValue"); } }, actions: { addValue() { - if (this.get('inputInvalid')) { return; } + if (this.get("inputInvalid")) { + return; + } - this.get('collection').addObject(this.get('newValue')); - this.set('newValue', ''); + this.get("collection").addObject(this.get("newValue")); + this.set("newValue", ""); this._saveValues(); }, removeValue(value) { - const collection = this.get('collection'); + const collection = this.get("collection"); collection.removeObject(value); this._saveValues(); } diff --git a/app/assets/javascripts/admin/components/watched-word-form.js.es6 b/app/assets/javascripts/admin/components/watched-word-form.js.es6 index e7ead641f2..c9a61965e8 100644 --- a/app/assets/javascripts/admin/components/watched-word-form.js.es6 +++ b/app/assets/javascripts/admin/components/watched-word-form.js.es6 @@ -1,65 +1,94 @@ -import WatchedWord from 'admin/models/watched-word'; -import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; +import WatchedWord from "admin/models/watched-word"; +import { + default as computed, + on, + observes +} from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNames: ['watched-word-form'], + classNames: ["watched-word-form"], formSubmitted: false, actionKey: null, showMessage: false, - @computed('regularExpressions') + @computed("regularExpressions") placeholderKey(regularExpressions) { - return "admin.watched_words.form.placeholder" + - (regularExpressions ? "_regexp" : ""); + return ( + "admin.watched_words.form.placeholder" + + (regularExpressions ? "_regexp" : "") + ); }, - @observes('word') + @observes("word") removeMessage() { - if (this.get('showMessage') && !Ember.isEmpty(this.get('word'))) { - this.set('showMessage', false); + if (this.get("showMessage") && !Ember.isEmpty(this.get("word"))) { + this.set("showMessage", false); } }, - @computed('word') + @computed("word") isUniqueWord(word) { const words = this.get("filteredContent") || []; - const filtered = words.filter(content => content.action === this.get("actionKey")); - return filtered.every(content => content.word.toLowerCase() !== word.toLowerCase()); + const filtered = words.filter( + content => content.action === this.get("actionKey") + ); + return filtered.every( + content => content.word.toLowerCase() !== word.toLowerCase() + ); }, actions: { submit() { if (!this.get("isUniqueWord")) { - this.setProperties({ showMessage: true, message: I18n.t('admin.watched_words.form.exists') }); + this.setProperties({ + showMessage: true, + message: I18n.t("admin.watched_words.form.exists") + }); return; } - if (!this.get('formSubmitted')) { - this.set('formSubmitted', true); + if (!this.get("formSubmitted")) { + this.set("formSubmitted", true); - const watchedWord = WatchedWord.create({ word: this.get('word'), action: this.get('actionKey') }); - - watchedWord.save().then(result => { - this.setProperties({ word: '', formSubmitted: false, showMessage: true, message: I18n.t('admin.watched_words.form.success') }); - this.sendAction('action', WatchedWord.create(result)); - Ember.run.schedule('afterRender', () => this.$('.watched-word-input').focus()); - }).catch(e => { - this.set('formSubmitted', false); - const msg = (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) ? - I18n.t("generic_error_with_reason", {error: e.jqXHR.responseJSON.errors.join('. ')}) : - I18n.t("generic_error"); - bootbox.alert(msg, () => this.$('.watched-word-input').focus()); + const watchedWord = WatchedWord.create({ + word: this.get("word"), + action: this.get("actionKey") }); + + watchedWord + .save() + .then(result => { + this.setProperties({ + word: "", + formSubmitted: false, + showMessage: true, + message: I18n.t("admin.watched_words.form.success") + }); + this.sendAction("action", WatchedWord.create(result)); + Ember.run.schedule("afterRender", () => + this.$(".watched-word-input").focus() + ); + }) + .catch(e => { + this.set("formSubmitted", false); + const msg = + e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors + ? I18n.t("generic_error_with_reason", { + error: e.jqXHR.responseJSON.errors.join(". ") + }) + : I18n.t("generic_error"); + bootbox.alert(msg, () => this.$(".watched-word-input").focus()); + }); } } }, @on("didInsertElement") _init() { - Ember.run.schedule('afterRender', () => { - this.$('.watched-word-input').keydown(e => { + Ember.run.schedule("afterRender", () => { + this.$(".watched-word-input").keydown(e => { if (e.keyCode === 13) { - this.send('submit'); + this.send("submit"); } }); }); diff --git a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 b/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 index 8d9e6ba0a9..e5ff9a1fd7 100644 --- a/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 +++ b/app/assets/javascripts/admin/components/watched-word-uploader.js.es6 @@ -2,16 +2,16 @@ import computed from "ember-addons/ember-computed-decorators"; import UploadMixin from "discourse/mixins/upload"; export default Em.Component.extend(UploadMixin, { - type: 'csv', - classNames: 'watched-words-uploader', - uploadUrl: '/admin/logs/watched_words/upload', + type: "csv", + classNames: "watched-words-uploader", + uploadUrl: "/admin/logs/watched_words/upload", addDisabled: Em.computed.alias("uploading"), validateUploadedFilesOptions() { return { csvOnly: true }; }, - @computed('actionKey') + @computed("actionKey") data(actionKey) { return { action_key: actionKey }; }, diff --git a/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6 b/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6 index 529538263c..69a1c321f7 100644 --- a/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-api-keys.js.es6 @@ -1,32 +1,40 @@ -import ApiKey from 'admin/models/api-key'; +import ApiKey from "admin/models/api-key"; export default Ember.Controller.extend({ - actions: { generateMasterKey() { - ApiKey.generateMasterKey().then(key => this.get('model').pushObject(key)); + ApiKey.generateMasterKey().then(key => this.get("model").pushObject(key)); }, regenerateKey(key) { - bootbox.confirm(I18n.t("admin.api.confirm_regen"), I18n.t("no_value"), I18n.t("yes_value"), result => { - if (result) { - key.regenerate(); + bootbox.confirm( + I18n.t("admin.api.confirm_regen"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + key.regenerate(); + } } - }); + ); }, revokeKey(key) { - bootbox.confirm(I18n.t("admin.api.confirm_revoke"), I18n.t("no_value"), I18n.t("yes_value"), result => { - if (result) { - key.revoke().then(() => this.get('model').removeObject(key)); + bootbox.confirm( + I18n.t("admin.api.confirm_revoke"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + key.revoke().then(() => this.get("model").removeObject(key)); + } } - }); + ); } }, // Has a master key already been generated? hasMasterKey: function() { - return !!this.get('model').findBy('user', null); - }.property('model.[]') - + return !!this.get("model").findBy("user", null); + }.property("model.[]") }); diff --git a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 index 67b6c58d06..49e8d9d441 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 @@ -1,13 +1,15 @@ -import { ajax } from 'discourse/lib/ajax'; +import { ajax } from "discourse/lib/ajax"; export default Ember.Controller.extend({ adminBackups: Ember.inject.controller(), - status: Ember.computed.alias('adminBackups.model'), + status: Ember.computed.alias("adminBackups.model"), - uploadLabel: function() { return I18n.t("admin.backups.upload.label"); }.property(), + uploadLabel: function() { + return I18n.t("admin.backups.upload.label"); + }.property(), restoreTitle: function() { - if (!this.get('status.allowRestore')) { + if (!this.get("status.allowRestore")) { return "admin.backups.operations.restore.is_disabled"; } else if (this.get("status.isOperationRunning")) { return "admin.backups.operations.is_running"; @@ -17,7 +19,6 @@ export default Ember.Controller.extend({ }.property("status.{allowRestore,isOperationRunning}"), actions: { - toggleReadOnlyMode() { var self = this; if (!this.site.get("isReadOnly")) { @@ -38,9 +39,8 @@ export default Ember.Controller.extend({ }, download(backup) { - let link = backup.get('filename'); - ajax("/admin/backups/" + link, { type: "PUT" }) - .then(() => { + let link = backup.get("filename"); + ajax("/admin/backups/" + link, { type: "PUT" }).then(() => { bootbox.alert(I18n.t("admin.backups.operations.download.alert")); }); } diff --git a/app/assets/javascripts/admin/controllers/admin-backups.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups.js.es6 index a429883378..5cfa57271e 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups.js.es6 @@ -1,5 +1,9 @@ export default Ember.Controller.extend({ noOperationIsRunning: Ember.computed.not("model.isOperationRunning"), - rollbackEnabled: Ember.computed.and("model.canRollback", "model.restoreEnabled", "noOperationIsRunning"), + rollbackEnabled: Ember.computed.and( + "model.canRollback", + "model.restoreEnabled", + "noOperationIsRunning" + ), rollbackDisabled: Ember.computed.not("rollbackEnabled") }); diff --git a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 index 7ddc6f3ac4..c0d58e3b11 100644 --- a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 @@ -1,108 +1,139 @@ -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import BufferedContent from 'discourse/mixins/buffered-content'; -import { propertyNotEqual } from 'discourse/lib/computed'; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import BufferedContent from "discourse/mixins/buffered-content"; +import { propertyNotEqual } from "discourse/lib/computed"; export default Ember.Controller.extend(BufferedContent, { adminBadges: Ember.inject.controller(), saving: false, - savingStatus: '', + savingStatus: "", - badgeTypes: Ember.computed.alias('adminBadges.badgeTypes'), - badgeGroupings: Ember.computed.alias('adminBadges.badgeGroupings'), - badgeTriggers: Ember.computed.alias('adminBadges.badgeTriggers'), - protectedSystemFields: Ember.computed.alias('adminBadges.protectedSystemFields'), + badgeTypes: Ember.computed.alias("adminBadges.badgeTypes"), + badgeGroupings: Ember.computed.alias("adminBadges.badgeGroupings"), + badgeTriggers: Ember.computed.alias("adminBadges.badgeTriggers"), + protectedSystemFields: Ember.computed.alias( + "adminBadges.protectedSystemFields" + ), - readOnly: Ember.computed.alias('buffered.system'), - showDisplayName: propertyNotEqual('name', 'displayName'), + readOnly: Ember.computed.alias("buffered.system"), + showDisplayName: propertyNotEqual("name", "displayName"), hasQuery: function() { - const bQuery = this.get('buffered.query'); + const bQuery = this.get("buffered.query"); if (bQuery) { return bQuery.trim().length > 0; } - const mQuery = this.get('model.query'); + const mQuery = this.get("model.query"); return mQuery && mQuery.trim().length > 0; - }.property('model.query', 'buffered.query'), + }.property("model.query", "buffered.query"), _resetSaving: function() { - this.set('saving', false); - this.set('savingStatus', ''); - }.observes('model.id'), + this.set("saving", false); + this.set("savingStatus", ""); + }.observes("model.id"), actions: { save() { - if (!this.get('saving')) { - let fields = ['allow_title', 'multiple_grant', - 'listable', 'auto_revoke', - 'enabled', 'show_posts', - 'target_posts', 'name', 'description', - 'long_description', - 'icon', 'image', 'query', 'badge_grouping_id', - 'trigger', 'badge_type_id']; + if (!this.get("saving")) { + let fields = [ + "allow_title", + "multiple_grant", + "listable", + "auto_revoke", + "enabled", + "show_posts", + "target_posts", + "name", + "description", + "long_description", + "icon", + "image", + "query", + "badge_grouping_id", + "trigger", + "badge_type_id" + ]; - if (this.get('buffered.system')){ - var protectedFields = this.get('protectedSystemFields'); - fields = _.filter(fields, function(f){ - return !_.include(protectedFields,f); + if (this.get("buffered.system")) { + var protectedFields = this.get("protectedSystemFields"); + fields = _.filter(fields, function(f) { + return !_.include(protectedFields, f); }); } - this.set('saving', true); - this.set('savingStatus', I18n.t('saving')); + this.set("saving", true); + this.set("savingStatus", I18n.t("saving")); - const boolFields = ['allow_title', 'multiple_grant', - 'listable', 'auto_revoke', - 'enabled', 'show_posts', - 'target_posts' ]; + const boolFields = [ + "allow_title", + "multiple_grant", + "listable", + "auto_revoke", + "enabled", + "show_posts", + "target_posts" + ]; const data = {}; - const buffered = this.get('buffered'); - fields.forEach(function(field){ + const buffered = this.get("buffered"); + fields.forEach(function(field) { var d = buffered.get(field); - if (_.include(boolFields, field)) { d = !!d; } + if (_.include(boolFields, field)) { + d = !!d; + } data[field] = d; }); - const newBadge = !this.get('id'); - const model = this.get('model'); - this.get('model').save(data).then(() => { - if (newBadge) { - const adminBadges = this.get('adminBadges.model'); - if (!adminBadges.includes(model)) { - adminBadges.pushObject(model); + const newBadge = !this.get("id"); + const model = this.get("model"); + this.get("model") + .save(data) + .then(() => { + if (newBadge) { + const adminBadges = this.get("adminBadges.model"); + if (!adminBadges.includes(model)) { + adminBadges.pushObject(model); + } + this.transitionToRoute("adminBadges.show", model.get("id")); + } else { + this.commitBuffer(); + this.set("savingStatus", I18n.t("saved")); } - this.transitionToRoute('adminBadges.show', model.get('id')); - } else { - this.commitBuffer(); - this.set('savingStatus', I18n.t('saved')); - } - - }).catch(popupAjaxError).finally(() => { - this.set('saving', false); - this.set('savingStatus', ''); - }); + }) + .catch(popupAjaxError) + .finally(() => { + this.set("saving", false); + this.set("savingStatus", ""); + }); } }, destroy() { - const adminBadges = this.get('adminBadges.model'); - const model = this.get('model'); + const adminBadges = this.get("adminBadges.model"); + const model = this.get("model"); - if (!model.get('id')) { - this.transitionToRoute('adminBadges.index'); + if (!model.get("id")) { + this.transitionToRoute("adminBadges.index"); return; } - return bootbox.confirm(I18n.t("admin.badges.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => { - if (result) { - model.destroy().then(() => { - adminBadges.removeObject(model); - this.transitionToRoute('adminBadges.index'); - }).catch(() => { - bootbox.alert(I18n.t('generic_error')); - }); + return bootbox.confirm( + I18n.t("admin.badges.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + model + .destroy() + .then(() => { + adminBadges.removeObject(model); + this.transitionToRoute("adminBadges.index"); + }) + .catch(() => { + bootbox.alert(I18n.t("generic_error")); + }); + } } - }); + ); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 index e333d2a58d..ca62835a04 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors-show.js.es6 @@ -1,7 +1,7 @@ -import computed from 'ember-addons/ember-computed-decorators'; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ - @computed("model.colors","onlyOverridden") + @computed("model.colors", "onlyOverridden") colors(allColors, onlyOverridden) { if (onlyOverridden) { return allColors.filter(color => color.get("overridden")); @@ -11,7 +11,6 @@ export default Ember.Controller.extend({ }, actions: { - revert: function(color) { color.revert(); }, @@ -28,14 +27,20 @@ export default Ember.Controller.extend({ let range = document.createRange(); range.selectNode(area[0]); window.getSelection().addRange(range); - let successful = document.execCommand('copy'); + let successful = document.execCommand("copy"); if (successful) { - this.set("model.savingStatus", I18n.t("admin.customize.copied_to_clipboard")); + this.set( + "model.savingStatus", + I18n.t("admin.customize.copied_to_clipboard") + ); } else { - this.set("model.savingStatus", I18n.t("admin.customize.copy_to_clipboard_error")); + this.set( + "model.savingStatus", + I18n.t("admin.customize.copy_to_clipboard_error") + ); } - setTimeout(()=>{ + setTimeout(() => { this.set("model.savingStatus", null); }, 2000); @@ -46,29 +51,38 @@ export default Ember.Controller.extend({ }, copy() { - var newColorScheme = Em.copy(this.get('model'), true); - newColorScheme.set('name', I18n.t('admin.customize.colors.copy_name_prefix') + ' ' + this.get('model.name')); - newColorScheme.save().then(()=>{ - this.get('allColors').pushObject(newColorScheme); - this.replaceRoute('adminCustomize.colors.show', newColorScheme); + var newColorScheme = Em.copy(this.get("model"), true); + newColorScheme.set( + "name", + I18n.t("admin.customize.colors.copy_name_prefix") + + " " + + this.get("model.name") + ); + newColorScheme.save().then(() => { + this.get("allColors").pushObject(newColorScheme); + this.replaceRoute("adminCustomize.colors.show", newColorScheme); }); }, save: function() { - this.get('model').save(); + this.get("model").save(); }, destroy: function() { - - const model = this.get('model'); - return bootbox.confirm(I18n.t("admin.customize.colors.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => { - if (result) { - model.destroy().then(()=>{ - this.get('allColors').removeObject(model); - this.replaceRoute('adminCustomize.colors'); - }); + const model = this.get("model"); + return bootbox.confirm( + I18n.t("admin.customize.colors.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + model.destroy().then(() => { + this.get("allColors").removeObject(model); + this.replaceRoute("adminCustomize.colors"); + }); + } } - }); + ); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 index 87166e386f..945a46acf3 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-colors.js.es6 @@ -1,41 +1,43 @@ -import showModal from 'discourse/lib/show-modal'; +import showModal from "discourse/lib/show-modal"; export default Ember.Controller.extend({ baseColorScheme: function() { - return this.get('model').findBy('is_base', true); - }.property('model.@each.id'), + return this.get("model").findBy("is_base", true); + }.property("model.@each.id"), baseColorSchemes: function() { - return this.get('model').filterBy('is_base', true); - }.property('model.@each.id'), + return this.get("model").filterBy("is_base", true); + }.property("model.@each.id"), baseColors: function() { var baseColorsHash = Em.Object.create({}); - _.each(this.get('baseColorScheme.colors'), function(color){ - baseColorsHash.set(color.get('name'), color); + _.each(this.get("baseColorScheme.colors"), function(color) { + baseColorsHash.set(color.get("name"), color); }); return baseColorsHash; - }.property('baseColorScheme'), + }.property("baseColorScheme"), actions: { - newColorSchemeWithBase(baseKey) { - const base = this.get('baseColorSchemes').findBy('base_scheme_id', baseKey); + const base = this.get("baseColorSchemes").findBy( + "base_scheme_id", + baseKey + ); const newColorScheme = Em.copy(base, true); - newColorScheme.set('name', I18n.t('admin.customize.colors.new_name')); - newColorScheme.set('base_scheme_id', base.get('base_scheme_id')); - newColorScheme.save().then(()=>{ - this.get('model').pushObject(newColorScheme); - newColorScheme.set('savingStatus', null); - this.replaceRoute('adminCustomize.colors.show', newColorScheme); + newColorScheme.set("name", I18n.t("admin.customize.colors.new_name")); + newColorScheme.set("base_scheme_id", base.get("base_scheme_id")); + newColorScheme.save().then(() => { + this.get("model").pushObject(newColorScheme); + newColorScheme.set("savingStatus", null); + this.replaceRoute("adminCustomize.colors.show", newColorScheme); }); }, newColorScheme() { - showModal('admin-color-scheme-select-base', { model: this.get('baseColorSchemes'), admin: true}); - }, - - + showModal("admin-color-scheme-select-base", { + model: this.get("baseColorSchemes"), + admin: true + }); + } } - }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 index 4c5a2a2854..5a20a94cbf 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-email-templates-edit.js.es6 @@ -1,38 +1,47 @@ -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import { bufferedProperty } from 'discourse/mixins/buffered-content'; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { bufferedProperty } from "discourse/mixins/buffered-content"; -export default Ember.Controller.extend(bufferedProperty('emailTemplate'), { +export default Ember.Controller.extend(bufferedProperty("emailTemplate"), { saved: false, hasMultipleSubjects: function() { - const buffered = this.get('buffered'); - if (buffered.getProperties('subject')['subject']) { + const buffered = this.get("buffered"); + if (buffered.getProperties("subject")["subject"]) { return false; } else { - return buffered.getProperties('id')['id']; + return buffered.getProperties("id")["id"]; } }.property("buffered"), actions: { saveChanges() { - this.set('saved', false); - const buffered = this.get('buffered'); - this.get('emailTemplate').save(buffered.getProperties('subject', 'body')).then(() => { - this.set('saved', true); - }).catch(popupAjaxError); + this.set("saved", false); + const buffered = this.get("buffered"); + this.get("emailTemplate") + .save(buffered.getProperties("subject", "body")) + .then(() => { + this.set("saved", true); + }) + .catch(popupAjaxError); }, revertChanges() { - this.set('saved', false); - bootbox.confirm(I18n.t('admin.customize.email_templates.revert_confirm'), result => { - if (result) { - this.get('emailTemplate').revert().then(props => { - const buffered = this.get('buffered'); - buffered.setProperties(props); - this.commitBuffer(); - }).catch(popupAjaxError); + this.set("saved", false); + bootbox.confirm( + I18n.t("admin.customize.email_templates.revert_confirm"), + result => { + if (result) { + this.get("emailTemplate") + .revert() + .then(props => { + const buffered = this.get("buffered"); + buffered.setProperties(props); + this.commitBuffer(); + }) + .catch(popupAjaxError); + } } - }); + ); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 index fe9cd69804..6fe215c9c3 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-email-templates.js.es6 @@ -1,6 +1,6 @@ export default Ember.Controller.extend({ - titleSorting: ['title'], + titleSorting: ["title"], emailTemplates: null, - sortedTemplates: Ember.computed.sort('emailTemplates', 'titleSorting') + sortedTemplates: Ember.computed.sort("emailTemplates", "titleSorting") }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 index 28ae718afa..33b46886db 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-edit.js.es6 @@ -1,66 +1,91 @@ -import { url } from 'discourse/lib/computed'; -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import { url } from "discourse/lib/computed"; +import { + default as computed, + observes +} from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ maximized: false, section: null, - editRouteName: 'adminCustomizeThemes.edit', + editRouteName: "adminCustomizeThemes.edit", targets: [ - { id: 0, name: 'common' }, - { id: 1, name: 'desktop' }, - { id: 2, name: 'mobile' }, - { id: 3, name: 'settings' } + { id: 0, name: "common" }, + { id: 1, name: "desktop" }, + { id: 2, name: "mobile" }, + { id: 3, name: "settings" } ], - fieldsForTarget: function (target) { - const common = ["scss", "head_tag", "header", "after_header", "body_tag", "footer"]; - switch(target) { - case "common": return [...common, "embedded_scss"]; - case "desktop": return common; - case "mobile": return common; - case "settings": return ["yaml"]; + fieldsForTarget: function(target) { + const common = [ + "scss", + "head_tag", + "header", + "after_header", + "body_tag", + "footer" + ]; + switch (target) { + case "common": + return [...common, "embedded_scss"]; + case "desktop": + return common; + case "mobile": + return common; + case "settings": + return ["yaml"]; } }, - @computed('onlyOverridden') + @computed("onlyOverridden") showCommon() { - return this.shouldShow('common'); + return this.shouldShow("common"); }, - @computed('onlyOverridden') + @computed("onlyOverridden") showDesktop() { - return this.shouldShow('desktop'); + return this.shouldShow("desktop"); }, - @computed('onlyOverridden') + @computed("onlyOverridden") showMobile() { - return this.shouldShow('mobile'); + return this.shouldShow("mobile"); }, - @computed('onlyOverridden', 'model.remote_theme') + @computed("onlyOverridden", "model.remote_theme") showSettings() { return false; }, - @observes('onlyOverridden') + @observes("onlyOverridden") onlyOverriddenChanged() { - if (this.get('onlyOverridden')) { - if (!this.get('model').hasEdited(this.get('currentTargetName'), this.get('fieldName'))) { - let target = (this.get('showCommon') && 'common') || - (this.get('showDesktop') && 'desktop') || - (this.get('showMobile') && 'mobile'); + if (this.get("onlyOverridden")) { + if ( + !this.get("model").hasEdited( + this.get("currentTargetName"), + this.get("fieldName") + ) + ) { + let target = + (this.get("showCommon") && "common") || + (this.get("showDesktop") && "desktop") || + (this.get("showMobile") && "mobile"); - let fields = this.get('model.theme_fields'); - let field = fields && fields.find(f => (f.target === target)); - this.replaceRoute(this.get('editRouteName'), this.get('model.id'), target, field && field.name); + let fields = this.get("model.theme_fields"); + let field = fields && fields.find(f => f.target === target); + this.replaceRoute( + this.get("editRouteName"), + this.get("model.id"), + target, + field && field.name + ); } } }, - shouldShow(target){ - if(!this.get("onlyOverridden")) { + shouldShow(target) { + if (!this.get("onlyOverridden")) { return true; } return this.get("model").hasEdited(target); @@ -69,13 +94,13 @@ export default Ember.Controller.extend({ currentTarget: 0, setTargetName: function(name) { - const target = this.get('targets').find(t => t.name === name); + const target = this.get("targets").find(t => t.name === name); this.set("currentTarget", target && target.id); }, @computed("currentTarget") currentTargetName(id) { - const target = this.get('targets').find(t => t.id === parseInt(id, 10)); + const target = this.get("targets").find(t => t.id === parseInt(id, 10)); return target && target.name; }, @@ -87,7 +112,7 @@ export default Ember.Controller.extend({ @computed("currentTargetName", "fieldName", "saving") error(target, fieldName) { - return this.get('model').getError(target, fieldName); + return this.get("model").getError(target, fieldName); }, @computed("fieldName", "currentTargetName") @@ -116,9 +141,9 @@ export default Ember.Controller.extend({ fields = fields.filter(name => model.hasEdited(targetName, name)); } - return fields.map(name=>{ + return fields.map(name => { let hash = { - key: (`admin.customize.theme.${name}.text`), + key: `admin.customize.theme.${name}.text`, name: name }; @@ -132,30 +157,36 @@ export default Ember.Controller.extend({ }); }, - previewUrl: url('model.id', '/admin/themes/%@/preview'), + previewUrl: url("model.id", "/admin/themes/%@/preview"), maximizeIcon: function() { - return this.get('maximized') ? 'compress' : 'expand'; - }.property('maximized'), + return this.get("maximized") ? "compress" : "expand"; + }.property("maximized"), saveButtonText: function() { - return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('admin.customize.save'); - }.property('model.isSaving'), + return this.get("model.isSaving") + ? I18n.t("saving") + : I18n.t("admin.customize.save"); + }.property("model.isSaving"), saveDisabled: function() { - return !this.get('model.changed') || this.get('model.isSaving'); - }.property('model.changed', 'model.isSaving'), + return !this.get("model.changed") || this.get("model.isSaving"); + }.property("model.changed", "model.isSaving"), actions: { save() { - this.set('saving', true); - this.get('model').saveChanges("theme_fields").finally(()=>{this.set('saving', false);}); + this.set("saving", true); + this.get("model") + .saveChanges("theme_fields") + .finally(() => { + this.set("saving", false); + }); }, toggleMaximize: function() { - this.toggleProperty('maximized'); - Em.run.next(()=>{ - this.appEvents.trigger('ace:resize'); + this.toggleProperty("maximized"); + Em.run.next(() => { + this.appEvents.trigger("ace:resize"); }); } } diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 index 16eff82947..20a16ab626 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes-show.js.es6 @@ -1,63 +1,72 @@ -import { default as computed } from 'ember-addons/ember-computed-decorators'; -import { url } from 'discourse/lib/computed'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; -import showModal from 'discourse/lib/show-modal'; -import ThemeSettings from 'admin/models/theme-settings'; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import { url } from "discourse/lib/computed"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import showModal from "discourse/lib/show-modal"; +import ThemeSettings from "admin/models/theme-settings"; const THEME_UPLOAD_VAR = 2; export default Ember.Controller.extend({ - - editRouteName: 'adminCustomizeThemes.edit', + editRouteName: "adminCustomizeThemes.edit", @computed("model", "allThemes") parentThemes(model, allThemes) { let parents = allThemes.filter(theme => - _.contains(theme.get("childThemes"), model)); + _.contains(theme.get("childThemes"), model) + ); return parents.length === 0 ? null : parents; }, @computed("model.theme_fields.@each") hasEditedFields(fields) { - return fields.any(f=>!Em.isBlank(f.value)); + return fields.any(f => !Em.isBlank(f.value)); }, - @computed('model.theme_fields.@each') + @computed("model.theme_fields.@each") editedDescriptions(fields) { let descriptions = []; let description = target => { - let current = fields.filter(field => field.target === target && !Em.isBlank(field.value)); + let current = fields.filter( + field => field.target === target && !Em.isBlank(field.value) + ); if (current.length > 0) { - let text = I18n.t('admin.customize.theme.'+target); - let localized = current.map(f=>I18n.t('admin.customize.theme.'+f.name + '.text')); + let text = I18n.t("admin.customize.theme." + target); + let localized = current.map(f => + I18n.t("admin.customize.theme." + f.name + ".text") + ); return text + ": " + localized.join(" , "); } }; - ['common', 'desktop', 'mobile'].forEach(target => { + ["common", "desktop", "mobile"].forEach(target => { descriptions.push(description(target)); }); - return descriptions.reject(d=>Em.isBlank(d)); + return descriptions.reject(d => Em.isBlank(d)); }, - previewUrl: url('model.id', '/admin/themes/%@/preview'), + previewUrl: url("model.id", "/admin/themes/%@/preview"), @computed("colorSchemeId", "model.color_scheme_id") colorSchemeChanged(colorSchemeId, existingId) { colorSchemeId = colorSchemeId === null ? null : parseInt(colorSchemeId); - return colorSchemeId !== existingId; + return colorSchemeId !== existingId; }, - @computed("availableChildThemes", "model.childThemes.@each", "model", "allowChildThemes") + @computed( + "availableChildThemes", + "model.childThemes.@each", + "model", + "allowChildThemes" + ) selectableChildThemes(available, childThemes, model, allowChildThemes) { if (!allowChildThemes && (!childThemes || childThemes.length === 0)) { return null; } let themes = []; - available.forEach(t=> { - if (!childThemes || (childThemes.indexOf(t) === -1)) { + available.forEach(t => { + if (!childThemes || childThemes.indexOf(t) === -1) { themes.push(t); - }; + } }); return themes.length === 0 ? null : themes; }, @@ -90,44 +99,48 @@ export default Ember.Controller.extend({ return settings.length > 0; }, - downloadUrl: url('model.id', '/admin/themes/%@'), + downloadUrl: url("model.id", "/admin/themes/%@"), actions: { - updateToLatest() { this.set("updatingRemote", true); - this.get("model").updateToLatest() + this.get("model") + .updateToLatest() .catch(popupAjaxError) - .finally(()=>{ + .finally(() => { this.set("updatingRemote", false); }); }, checkForThemeUpdates() { this.set("updatingRemote", true); - this.get("model").checkForUpdates() + this.get("model") + .checkForUpdates() .catch(popupAjaxError) - .finally(()=>{ + .finally(() => { this.set("updatingRemote", false); }); }, addUploadModal() { - showModal('admin-add-upload', {admin: true, name: ''}); + showModal("admin-add-upload", { admin: true, name: "" }); }, addUpload(info) { let model = this.get("model"); - model.setField('common', info.name, '', info.upload_id, THEME_UPLOAD_VAR); - model.saveChanges('theme_fields').catch(e => popupAjaxError(e)); + model.setField("common", info.name, "", info.upload_id, THEME_UPLOAD_VAR); + model.saveChanges("theme_fields").catch(e => popupAjaxError(e)); }, cancelChangeScheme() { this.set("colorSchemeId", this.get("model.color_scheme_id")); }, - changeScheme(){ + changeScheme() { let schemeId = this.get("colorSchemeId"); - this.set("model.color_scheme_id", schemeId === null ? null : parseInt(schemeId)); + this.set( + "model.color_scheme_id", + schemeId === null ? null : parseInt(schemeId) + ); this.get("model").saveChanges("color_scheme_id"); }, startEditingName() { @@ -144,14 +157,23 @@ export default Ember.Controller.extend({ }, editTheme() { - let edit = ()=>this.transitionToRoute(this.get('editRouteName'), this.get('model.id'), 'common', 'scss'); + let edit = () => + this.transitionToRoute( + this.get("editRouteName"), + this.get("model.id"), + "common", + "scss" + ); if (this.get("model.remote_theme")) { - bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => { - if (result) { - edit(); - } - }); + bootbox.confirm( + I18n.t("admin.customize.theme.edit_confirm"), + result => { + if (result) { + edit(); + } + } + ); } else { edit(); } @@ -159,10 +181,10 @@ export default Ember.Controller.extend({ applyDefault() { const model = this.get("model"); - model.saveChanges("default").then(()=>{ + model.saveChanges("default").then(() => { if (model.get("default")) { - this.get("allThemes").forEach(theme=>{ - if (theme !== model && theme.get('default')) { + this.get("allThemes").forEach(theme => { + if (theme !== model && theme.get("default")) { theme.set("default", false); } }); @@ -182,13 +204,15 @@ export default Ember.Controller.extend({ removeUpload(upload) { return bootbox.confirm( - I18n.t("admin.customize.theme.delete_upload_confirm"), - I18n.t("no_value"), - I18n.t("yes_value"), result => { - if (result) { - this.get("model").removeField(upload); - } - }); + I18n.t("admin.customize.theme.delete_upload_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + this.get("model").removeField(upload); + } + } + ); }, removeChildTheme(theme) { @@ -196,17 +220,20 @@ export default Ember.Controller.extend({ }, destroy() { - return bootbox.confirm(I18n.t("admin.customize.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), result => { - if (result) { - const model = this.get('model'); - model.destroyRecord().then(() => { - this.get('allThemes').removeObject(model); - this.transitionToRoute('adminCustomizeThemes'); - }); + return bootbox.confirm( + I18n.t("admin.customize.delete_confirm"), + I18n.t("no_value"), + I18n.t("yes_value"), + result => { + if (result) { + const model = this.get("model"); + model.destroyRecord().then(() => { + this.get("allThemes").removeObject(model); + this.transitionToRoute("adminCustomizeThemes"); + }); + } } - }); - }, - + ); + } } - }); diff --git a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 b/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 index b9a897a26a..ecd8001895 100644 --- a/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-customize-themes.js.es6 @@ -1,10 +1,14 @@ -import { default as computed } from 'ember-addons/ember-computed-decorators'; +import { default as computed } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ - @computed('model', 'model.@each') + @computed("model", "model.@each") sortedThemes(themes) { return _.sortBy(themes.content, t => { - return [!t.get("default"), !t.get("user_selectable"), t.get("name").toLowerCase()]; + return [ + !t.get("default"), + !t.get("user_selectable"), + t.get("name").toLowerCase() + ]; }); } }); diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 index 2f24cf9edd..e809df6951 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard-next.js.es6 @@ -15,54 +15,61 @@ export default Ember.Controller.extend({ exceptionController: Ember.inject.controller("exception"), showVersionChecks: setting("version_checks"), diskSpace: Ember.computed.alias("model.attributes.disk_space"), + lastBackupTakenAt: Ember.computed.alias( + "model.attributes.last_backup_taken_at" + ), logSearchQueriesEnabled: setting("log_search_queries"), availablePeriods: ["yearly", "quarterly", "monthly", "weekly"], + shouldDisplayDurability: Ember.computed.and("lastBackupTakenAt", "diskSpace"), @computed("problems.length") foundProblems(problemsLength) { return this.currentUser.get("admin") && (problemsLength || 0) > 0; }, - @computed("foundProblems") - thereWereProblems(foundProblems) { - if (!this.currentUser.get("admin")) { return false; } - - if (foundProblems) { - this.set("hadProblems", true); - return true; - } else { - return this.get("hadProblems") || false; - } - }, - fetchDashboard() { if (this.get("isLoading")) return; - if (!this.get("dashboardFetchedAt") || moment().subtract(30, "minutes").toDate() > this.get("dashboardFetchedAt")) { + if ( + !this.get("dashboardFetchedAt") || + moment() + .subtract(30, "minutes") + .toDate() > this.get("dashboardFetchedAt") + ) { this.set("isLoading", true); const versionChecks = this.siteSettings.version_checks; - AdminDashboardNext.find().then(adminDashboardNextModel => { + AdminDashboardNext.find() + .then(adminDashboardNextModel => { + if (versionChecks) { + this.set( + "versionCheck", + VersionCheck.create(adminDashboardNextModel.version_check) + ); + } - if (versionChecks) { - this.set("versionCheck", VersionCheck.create(adminDashboardNextModel.version_check)); - } - - this.setProperties({ - dashboardFetchedAt: new Date(), - model: adminDashboardNextModel, - reports: adminDashboardNextModel.reports.map(x => Report.create(x)) + this.setProperties({ + dashboardFetchedAt: new Date(), + model: adminDashboardNextModel, + reports: adminDashboardNextModel.reports.map(x => Report.create(x)) + }); + }) + .catch(e => { + this.get("exceptionController").set("thrown", e.jqXHR); + this.replaceRoute("exception"); + }) + .finally(() => { + this.set("isLoading", false); }); - }).catch(e => { - this.get("exceptionController").set("thrown", e.jqXHR); - this.replaceRoute("exception"); - }).finally(() => { - this.set("isLoading", false); - }); } - if (!this.get("problemsFetchedAt") || moment().subtract(PROBLEMS_CHECK_MINUTES, "minutes").toDate() > this.get("problemsFetchedAt")) { + if ( + !this.get("problemsFetchedAt") || + moment() + .subtract(PROBLEMS_CHECK_MINUTES, "minutes") + .toDate() > this.get("problemsFetchedAt") + ) { this.loadProblems(); } }, @@ -70,21 +77,28 @@ export default Ember.Controller.extend({ loadProblems() { this.set("loadingProblems", true); this.set("problemsFetchedAt", new Date()); - AdminDashboardNext.fetchProblems().then(d => { - this.set("problems", d.problems); - }).finally(() => { - this.set("loadingProblems", false); - }); + AdminDashboardNext.fetchProblems() + .then(d => { + this.set("problems", d.problems); + }) + .finally(() => { + this.set("loadingProblems", false); + }); }, @computed("problemsFetchedAt") problemsTimestamp(problemsFetchedAt) { - return moment(problemsFetchedAt).locale("en").format("LLL"); + return moment(problemsFetchedAt) + .locale("en") + .format("LLL"); }, @computed("period") startDate(period) { - let fullDay = moment().locale("en").utc().subtract(1, "day"); + let fullDay = moment() + .locale("en") + .utc() + .subtract(1, "day"); switch (period) { case "yearly": @@ -106,20 +120,28 @@ export default Ember.Controller.extend({ @computed() lastWeek() { - return moment().locale("en").utc().endOf("day").subtract(1, "week"); + return moment() + .locale("en") + .utc() + .endOf("day") + .subtract(1, "week"); }, @computed() endDate() { - return moment().locale("en").utc().subtract(1, "day").endOf("day"); + return moment() + .locale("en") + .utc() + .subtract(1, "day") + .endOf("day"); }, - @computed("updated_at") + @computed("model.attributes.updated_at") updatedTimestamp(updatedAt) { return moment(updatedAt).format("LLL"); }, - @computed("last_backup_taken_at") + @computed("lastBackupTakenAt") backupTimestamp(lastBackupTakenAt) { return moment(lastBackupTakenAt).format("LLL"); }, @@ -130,7 +152,7 @@ export default Ember.Controller.extend({ }, refreshProblems() { this.loadProblems(); - }, + } }, _reportsForPeriodURL(period) { diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 index 973cd3d57c..9b545fc2fb 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 @@ -1,50 +1,74 @@ -import AdminDashboard from 'admin/models/admin-dashboard'; -import Report from 'admin/models/report'; -import AdminUser from 'admin/models/admin-user'; -import computed from 'ember-addons/ember-computed-decorators'; +import AdminDashboard from "admin/models/admin-dashboard"; +import Report from "admin/models/report"; +import AdminUser from "admin/models/admin-user"; +import computed from "ember-addons/ember-computed-decorators"; +const ATTRIBUTES = [ + "disk_space", + "admins", + "moderators", + "silenced", + "suspended", + "top_traffic_sources", + "top_referred_topics", + "updated_at" +]; -const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'silenced', 'suspended', 'top_traffic_sources', - 'top_referred_topics', 'updated_at']; - -const REPORTS = [ 'global_reports', 'page_view_reports', 'private_message_reports', 'http_reports', - 'user_reports', 'mobile_reports']; +const REPORTS = [ + "global_reports", + "page_view_reports", + "private_message_reports", + "http_reports", + "user_reports", + "mobile_reports" +]; // This controller supports the default interface when you enter the admin section. export default Ember.Controller.extend({ loading: null, versionCheck: null, dashboardFetchedAt: null, - exceptionController: Ember.inject.controller('exception'), + exceptionController: Ember.inject.controller("exception"), fetchDashboard() { - if (!this.get('dashboardFetchedAt') || moment().subtract(30, 'minutes').toDate() > this.get('dashboardFetchedAt')) { - this.set('loading', true); - AdminDashboard.find().then(d => { - this.set('dashboardFetchedAt', new Date()); + if ( + !this.get("dashboardFetchedAt") || + moment() + .subtract(30, "minutes") + .toDate() > this.get("dashboardFetchedAt") + ) { + this.set("loading", true); + AdminDashboard.find() + .then(d => { + this.set("dashboardFetchedAt", new Date()); - REPORTS.forEach(name => this.set(name, d[name].map(r => Report.create(r)))); + REPORTS.forEach(name => + this.set(name, d[name].map(r => Report.create(r))) + ); - const topReferrers = d.top_referrers; - if (topReferrers && topReferrers.data) { - d.top_referrers.data = topReferrers.data.map(user => AdminUser.create(user)); - this.set('top_referrers', topReferrers); - } + const topReferrers = d.top_referrers; + if (topReferrers && topReferrers.data) { + d.top_referrers.data = topReferrers.data.map(user => + AdminUser.create(user) + ); + this.set("top_referrers", topReferrers); + } - ATTRIBUTES.forEach(a => this.set(a, d[a])); - }).catch(e => { - this.get('exceptionController').set('thrown', e.jqXHR); - this.replaceRoute('exception'); - }).finally(() => { - this.set('loading', false); - }); + ATTRIBUTES.forEach(a => this.set(a, d[a])); + }) + .catch(e => { + this.get("exceptionController").set("thrown", e.jqXHR); + this.replaceRoute("exception"); + }) + .finally(() => { + this.set("loading", false); + }); } }, - - @computed('updated_at') + @computed("updated_at") updatedTimestamp(updatedAt) { - return moment(updatedAt).format('LLL'); + return moment(updatedAt).format("LLL"); }, actions: { @@ -52,5 +76,4 @@ export default Ember.Controller.extend({ this.set("showTrafficReport", true); } } - }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 index ae75d18715..230fa07afb 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-bounced.js.es6 @@ -1,6 +1,6 @@ -import AdminEmailLogsController from 'admin/controllers/admin-email-logs'; -import debounce from 'discourse/lib/debounce'; -import EmailLog from 'admin/models/email-log'; +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import debounce from "discourse/lib/debounce"; +import EmailLog from "admin/models/email-log"; export default AdminEmailLogsController.extend({ filterEmailLogs: debounce(function() { diff --git a/app/assets/javascripts/admin/controllers/admin-email-incomings.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-incomings.js.es6 index a6acd9af78..9dc59589fb 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-incomings.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-incomings.js.es6 @@ -1,21 +1,25 @@ -import IncomingEmail from 'admin/models/incoming-email'; +import IncomingEmail from "admin/models/incoming-email"; export default Ember.Controller.extend({ loading: false, actions: { - loadMore() { - if (this.get("loading") || this.get("model.allLoaded")) { return; } - this.set('loading', true); + if (this.get("loading") || this.get("model.allLoaded")) { + return; + } + this.set("loading", true); IncomingEmail.findAll(this.get("filter"), this.get("model.length")) - .then(incoming => { - if (incoming.length < 50) { this.get("model").set("allLoaded", true); } - this.get("model").addObjects(incoming); - }).finally(() => { - this.set('loading', false); - }); + .then(incoming => { + if (incoming.length < 50) { + this.get("model").set("allLoaded", true); + } + this.get("model").addObjects(incoming); + }) + .finally(() => { + this.set("loading", false); + }); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 index fce0cb891e..27b7bb498c 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 @@ -1,12 +1,11 @@ -import { ajax } from 'discourse/lib/ajax'; +import { ajax } from "discourse/lib/ajax"; export default Ember.Controller.extend({ - /** Is the "send test email" button disabled? @property sendTestEmailDisabled **/ - sendTestEmailDisabled: Em.computed.empty('testEmailAddress'), + sendTestEmailDisabled: Em.computed.empty("testEmailAddress"), /** Clears the 'sentTestEmail' property on successful send. @@ -14,8 +13,8 @@ export default Ember.Controller.extend({ @method testEmailAddressChanged **/ testEmailAddressChanged: function() { - this.set('sentTestEmail', false); - }.observes('testEmailAddress'), + this.set("sentTestEmail", false); + }.observes("testEmailAddress"), actions: { /** @@ -31,21 +30,28 @@ export default Ember.Controller.extend({ var self = this; ajax("/admin/email/test", { - type: 'POST', - data: { email_address: this.get('testEmailAddress') } - }).then(function () { - self.set('sentTestEmail', true); - }, function(e) { - if (e.responseJSON && e.responseJSON.errors) { - bootbox.alert(I18n.t('admin.email.error', { server_error: e.responseJSON.errors[0] })); - } else { - bootbox.alert(I18n.t('admin.email.test_error')); - } - }).finally(function() { - self.set('sendingEmail', false); - }); - + type: "POST", + data: { email_address: this.get("testEmailAddress") } + }) + .then( + function() { + self.set("sentTestEmail", true); + }, + function(e) { + if (e.responseJSON && e.responseJSON.errors) { + bootbox.alert( + I18n.t("admin.email.error", { + server_error: e.responseJSON.errors[0] + }) + ); + } else { + bootbox.alert(I18n.t("admin.email.test_error")); + } + } + ) + .finally(function() { + self.set("sendingEmail", false); + }); } } - }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 index 44a38dfa32..a86ae42111 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-logs.js.es6 @@ -1,20 +1,25 @@ -import EmailLog from 'admin/models/email-log'; +import EmailLog from "admin/models/email-log"; export default Ember.Controller.extend({ loading: false, actions: { loadMore() { - if (this.get("loading") || this.get("model.allLoaded")) { return; } + if (this.get("loading") || this.get("model.allLoaded")) { + return; + } - this.set('loading', true); + this.set("loading", true); return EmailLog.findAll(this.get("filter"), this.get("model.length")) - .then(logs => { - if (logs.length < 50) { this.get("model").set("allLoaded", true); } - this.get("model").addObjects(logs); - }).finally(() => { - this.set('loading', false); - }); + .then(logs => { + if (logs.length < 50) { + this.get("model").set("allLoaded", true); + } + this.get("model").addObjects(logs); + }) + .finally(() => { + this.set("loading", false); + }); } } }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 index eb03301c92..9517a3f9a7 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 @@ -1,52 +1,60 @@ -import EmailPreview from 'admin/models/email-preview'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; +import EmailPreview from "admin/models/email-preview"; +import { popupAjaxError } from "discourse/lib/ajax-error"; export default Ember.Controller.extend({ username: null, lastSeen: null, - emailEmpty: Ember.computed.empty('email'), - sendEmailDisabled: Ember.computed.or('emailEmpty', 'sendingEmail'), - showSendEmailForm: Ember.computed.notEmpty('model.html_content'), - htmlEmpty: Ember.computed.empty('model.html_content'), + emailEmpty: Ember.computed.empty("email"), + sendEmailDisabled: Ember.computed.or("emailEmpty", "sendingEmail"), + showSendEmailForm: Ember.computed.notEmpty("model.html_content"), + htmlEmpty: Ember.computed.empty("model.html_content"), actions: { refresh() { - const model = this.get('model'); + const model = this.get("model"); - this.set('loading', true); - this.set('sentEmail', false); + this.set("loading", true); + this.set("sentEmail", false); - let username = this.get('username'); + let username = this.get("username"); if (!username) { - username = this.currentUser.get('username'); - this.set('username', username); + username = this.currentUser.get("username"); + this.set("username", username); } - EmailPreview.findDigest(username, this.get('lastSeen')).then(email => { - model.setProperties(email.getProperties('html_content', 'text_content')); - this.set('loading', false); + EmailPreview.findDigest(username, this.get("lastSeen")).then(email => { + model.setProperties( + email.getProperties("html_content", "text_content") + ); + this.set("loading", false); }); }, toggleShowHtml() { - this.toggleProperty('showHtml'); + this.toggleProperty("showHtml"); }, sendEmail() { - this.set('sendingEmail', true); - this.set('sentEmail', false); + this.set("sendingEmail", true); + this.set("sentEmail", false); - EmailPreview.sendDigest(this.get('username'), this.get('lastSeen'), this.get('email')).then(result => { - if (result.errors) { - bootbox.alert(result.errors); - } else { - this.set('sentEmail', true); - } - }).catch(popupAjaxError).finally(() => { - this.set('sendingEmail', false); - }); + EmailPreview.sendDigest( + this.get("username"), + this.get("lastSeen"), + this.get("email") + ) + .then(result => { + if (result.errors) { + bootbox.alert(result.errors); + } else { + this.set("sentEmail", true); + } + }) + .catch(popupAjaxError) + .finally(() => { + this.set("sendingEmail", false); + }); } } - }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 index 69ebd5e4c4..a5f240fdbf 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-received.js.es6 @@ -1,9 +1,11 @@ -import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings'; -import debounce from 'discourse/lib/debounce'; -import IncomingEmail from 'admin/models/incoming-email'; +import AdminEmailIncomingsController from "admin/controllers/admin-email-incomings"; +import debounce from "discourse/lib/debounce"; +import IncomingEmail from "admin/models/incoming-email"; export default AdminEmailIncomingsController.extend({ filterIncomingEmails: debounce(function() { - IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings)); + IncomingEmail.findAll(this.get("filter")).then(incomings => + this.set("model", incomings) + ); }, 250).observes("filter.{from,to,subject}") }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 index 317a669cd0..b9341dd7e3 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-rejected.js.es6 @@ -1,9 +1,11 @@ -import AdminEmailIncomingsController from 'admin/controllers/admin-email-incomings'; -import debounce from 'discourse/lib/debounce'; -import IncomingEmail from 'admin/models/incoming-email'; +import AdminEmailIncomingsController from "admin/controllers/admin-email-incomings"; +import debounce from "discourse/lib/debounce"; +import IncomingEmail from "admin/models/incoming-email"; export default AdminEmailIncomingsController.extend({ filterIncomingEmails: debounce(function() { - IncomingEmail.findAll(this.get("filter")).then(incomings => this.set("model", incomings)); + IncomingEmail.findAll(this.get("filter")).then(incomings => + this.set("model", incomings) + ); }, 250).observes("filter.{from,to,subject,error}") }); diff --git a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 index d73d640adc..691b017c6f 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 @@ -1,6 +1,6 @@ -import AdminEmailLogsController from 'admin/controllers/admin-email-logs'; -import debounce from 'discourse/lib/debounce'; -import EmailLog from 'admin/models/email-log'; +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import debounce from "discourse/lib/debounce"; +import EmailLog from "admin/models/email-log"; export default AdminEmailLogsController.extend({ filterEmailLogs: debounce(function() { diff --git a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 index ae75d18715..230fa07afb 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 @@ -1,6 +1,6 @@ -import AdminEmailLogsController from 'admin/controllers/admin-email-logs'; -import debounce from 'discourse/lib/debounce'; -import EmailLog from 'admin/models/email-log'; +import AdminEmailLogsController from "admin/controllers/admin-email-logs"; +import debounce from "discourse/lib/debounce"; +import EmailLog from "admin/models/email-log"; export default AdminEmailLogsController.extend({ filterEmailLogs: debounce(function() { diff --git a/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 b/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 index 266aa6975e..33240955b5 100644 --- a/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 @@ -1,22 +1,20 @@ -import computed from 'ember-addons/ember-computed-decorators'; -import { popupAjaxError } from 'discourse/lib/ajax-error'; +import computed from "ember-addons/ember-computed-decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; export default Ember.Controller.extend({ saved: false, embedding: null, // show settings if we have at least one created host - @computed('embedding.embeddable_hosts.@each.isCreated') + @computed("embedding.embeddable_hosts.@each.isCreated") showSecondary() { - const hosts = this.get('embedding.embeddable_hosts'); - return hosts.length && hosts.findBy('isCreated'); + const hosts = this.get("embedding.embeddable_hosts"); + return hosts.length && hosts.findBy("isCreated"); }, - @computed('embedding.base_url') + @computed("embedding.base_url") embeddingCode(baseUrl) { - - const html = -`
+ const html = `
]\n- A\n- B\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to match("data-poll-") expect(json["cooked"]).to include("<script>") @@ -111,7 +111,7 @@ describe PostsController do title: title, raw: "[Polls are awesome](/foobar)\n[poll]\n- A\n- B\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to match("data-poll-") expect(json["polls"]).to be @@ -122,7 +122,7 @@ describe PostsController do title: title, raw: "[poll name=1]\n- A\n[poll name=2]\n- B\n- C\n[/poll]\n- D\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to match("data-poll-") expect(json["polls"]["1"]).to_not be @@ -148,7 +148,7 @@ describe PostsController do id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["polls"]["poll"]["options"][2]["html"]).to eq("C") end @@ -160,7 +160,7 @@ describe PostsController do id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["polls_votes"]).to_not be end @@ -196,7 +196,7 @@ describe PostsController do id: post_id, post: { raw: new_option } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["polls"]["poll"]["options"][1]["html"]).to eq("C") end @@ -208,14 +208,14 @@ describe PostsController do id: post_id, post: { raw: new_option } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["polls"]["poll"]["options"][1]["html"]).to eq("C") end it "support changes on the post" do put :update, params: { id: post_id, post: { raw: updated } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["cooked"]).to match("before") end @@ -248,7 +248,7 @@ describe PostsController do id: post_id, post: { raw: new_option } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["polls"]["poll"]["options"][1]["html"]).to eq("C") expect(json["post"]["polls"]["poll"]["voters"]).to eq(1) @@ -267,7 +267,7 @@ describe PostsController do id: post_id, post: { raw: new_option } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["polls"]["poll"]["options"][1]["html"]).to eq("C") @@ -278,7 +278,7 @@ describe PostsController do it "support changes on the post" do put :update, params: { id: post_id, post: { raw: updated } }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["post"]["cooked"]).to match("before") end @@ -322,7 +322,7 @@ describe PostsController do title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to match("data-poll-") expect(json["polls"]["poll"]).to be @@ -363,7 +363,7 @@ describe PostsController do title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to eq("

[poll]

\n") end @@ -399,7 +399,7 @@ describe PostsController do title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to match("data-poll-") expect(json["polls"]["poll"]).to be @@ -418,7 +418,7 @@ describe PostsController do title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to match("data-poll-") expect(json["polls"]["poll"]).to be @@ -437,7 +437,7 @@ describe PostsController do title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json["cooked"]).to match("data-poll-") expect(json["polls"]["poll"]).to be diff --git a/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 index 99f4fd300c..586f6b9bab 100644 --- a/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/poll-builder-disabled-test.js.es6 @@ -13,32 +13,41 @@ acceptance("Poll Builder - polls are disabled", { } }); -test("regular user - sufficient trust level", (assert) => { +test("regular user - sufficient trust level", assert => { replaceCurrentUser({ staff: false, trust_level: 3 }); displayPollBuilderButton(); andThen(() => { - assert.ok(!exists(".select-kit-row[title='Build Poll']"), "it hides the builder button"); + assert.ok( + !exists(".select-kit-row[title='Build Poll']"), + "it hides the builder button" + ); }); }); -test("regular user - insufficient trust level", (assert) => { +test("regular user - insufficient trust level", assert => { replaceCurrentUser({ staff: false, trust_level: 1 }); displayPollBuilderButton(); andThen(() => { - assert.ok(!exists(".select-kit-row[title='Build Poll']"), "it hides the builder button"); + assert.ok( + !exists(".select-kit-row[title='Build Poll']"), + "it hides the builder button" + ); }); }); -test("staff", (assert) => { +test("staff", assert => { replaceCurrentUser({ staff: true }); displayPollBuilderButton(); andThen(() => { - assert.ok(!exists(".select-kit-row[title='Build Poll']"), "it hides the builder button"); + assert.ok( + !exists(".select-kit-row[title='Build Poll']"), + "it hides the builder button" + ); }); }); diff --git a/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 index 5f407a958f..933e62ab9d 100644 --- a/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/poll-builder-enabled-test.js.es6 @@ -13,32 +13,41 @@ acceptance("Poll Builder - polls are enabled", { } }); -test("regular user - sufficient trust level", (assert) => { +test("regular user - sufficient trust level", assert => { replaceCurrentUser({ staff: false, trust_level: 1 }); displayPollBuilderButton(); andThen(() => { - assert.ok(exists(".select-kit-row[title='Build Poll']"), "it shows the builder button"); + assert.ok( + exists(".select-kit-row[title='Build Poll']"), + "it shows the builder button" + ); }); }); -test("regular user - insufficient trust level", (assert) => { +test("regular user - insufficient trust level", assert => { replaceCurrentUser({ staff: false, trust_level: 0 }); displayPollBuilderButton(); andThen(() => { - assert.ok(!exists(".select-kit-row[title='Build Poll']"), "it hides the builder button"); + assert.ok( + !exists(".select-kit-row[title='Build Poll']"), + "it hides the builder button" + ); }); }); -test("staff - with insufficient trust level", (assert) => { +test("staff - with insufficient trust level", assert => { replaceCurrentUser({ staff: true, trust_level: 0 }); displayPollBuilderButton(); andThen(() => { - assert.ok(exists(".select-kit-row[title='Build Poll']"), "it shows the builder button"); + assert.ok( + exists(".select-kit-row[title='Build Poll']"), + "it shows the builder button" + ); }); }); diff --git a/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 index b6f81eba61..f32937ea97 100644 --- a/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 +++ b/plugins/poll/test/javascripts/acceptance/polls-test.js.es6 @@ -9,42 +9,1320 @@ acceptance("Rendering polls", { } }); -test("Single Poll", (assert) => { - server.get('/t/13.json', () => { // eslint-disable-line no-undef +test("Single Poll", assert => { + // prettier-ignore + server.get("/t/13.json", () => { // eslint-disable-line no-undef return [ 200, { "Content-Type": "application/json" }, - {"post_stream":{"posts":[{"id":19,"name":null,"username":"tgx","avatar_template":"/images/avatar.png","created_at":"2016-12-01T02:39:49.199Z","cooked":"
\n
\n
    \n
  • test
  • \n
  • haha
  • \n
\n

0voters

\n
\n
Show results
\n
\n\n
\n
\n
    \n
  • donkey
  • \n
  • kong
  • \n
\n

0voters

\n
\n
Show results
\n
","post_number":1,"post_type":1,"updated_at":"2016-12-01T02:47:18.317Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":13,"topic_slug":"this-is-a-test-topic-for-polls","display_username":null,"primary_group_name":null,"primary_group_flair_url":null,"primary_group_flair_bg_color":null,"primary_group_flair_color":null,"version":2,"can_edit":true,"can_delete":false,"can_recover":true,"can_wiki":true,"read":true,"user_title":null,"actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false,"polls":{"poll":{"options":[{"id":"57ddd734344eb7436d64a7d68a0df444","html":"test","votes":0},{"id":"b5b78d79ab5b5d75d4d33d8b87f5d2aa","html":"haha","votes":0}],"voters":2,"status":"open","name":"poll"},"test":{"options":[{"id":"c26ad90783b0d80936e5fdb292b7963c","html":"donkey","votes":0},{"id":"99f2b9ac452ba73b115fcf3556e6d2d4","html":"kong","votes":0}],"voters":3,"status":"open","name":"test"}}}],"stream":[19]},"timeline_lookup":[[1,0]],"id":13,"title":"This is a test topic for polls","fancy_title":"This is a test topic for polls","posts_count":1,"created_at":"2016-12-01T02:39:48.055Z","views":1,"reply_count":0,"participant_count":1,"like_count":0,"last_posted_at":"2016-12-01T02:39:49.199Z","visible":true,"closed":false,"archived":false,"has_summary":false,"archetype":"regular","slug":"this-is-a-test-topic-for-polls","category_id":1,"word_count":10,"deleted_at":null,"user_id":1,"draft":null,"draft_key":"topic_13","draft_sequence":4,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"pinned_until":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"last_poster":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"participants":[{"id":1,"username":"tgx","avatar_template":"/images/avatar.png","post_count":1}],"suggested_topics":[{"id":8,"title":"Welcome to Discourse","fancy_title":"Welcome to Discourse","slug":"welcome-to-discourse","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2016-11-24T02:10:54.328Z","last_posted_at":"2016-11-24T02:10:54.393Z","bumped":true,"bumped_at":"2016-11-24T02:10:54.393Z","unseen":false,"pinned":true,"unpinned":null,"excerpt":"The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …","visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":0,"category_id":1,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":-1,"username":"system","avatar_template":"/images/avatar.png"}}]},{"id":12,"title":"Some testing topic testing","fancy_title":"Some testing topic testing","slug":"some-testing-topic-testing","posts_count":4,"reply_count":0,"highest_post_number":4,"image_url":null,"created_at":"2016-11-24T08:36:08.773Z","last_posted_at":"2016-12-01T01:15:52.008Z","bumped":true,"bumped_at":"2016-12-01T01:15:52.008Z","unseen":false,"last_read_post_number":4,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":2,"category_id":1,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}]},{"id":11,"title":"Some testing topic","fancy_title":"Some testing topic","slug":"some-testing-topic","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2016-11-24T08:35:26.758Z","last_posted_at":"2016-11-24T08:35:26.894Z","bumped":true,"bumped_at":"2016-11-24T08:35:26.894Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":0,"category_id":1,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}]}],"notification_level":3,"notifications_reason_id":1,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":1,"last_read_post_number":1,"last_read_post_id":19,"deleted_by":null,"has_deleted":false,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false} + { + post_stream: { + posts: [ + { + id: 19, + name: null, + username: "tgx", + avatar_template: "/images/avatar.png", + created_at: "2016-12-01T02:39:49.199Z", + cooked: + '
\n
\n
    \n
  • test
  • \n
  • haha
  • \n
\n

0voters

\n
\n
Show results
\n
\n\n
\n
\n
    \n
  • donkey
  • \n
  • kong
  • \n
\n

0voters

\n
\n
Show results
\n
', + post_number: 1, + post_type: 1, + updated_at: "2016-12-01T02:47:18.317Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + avg_time: null, + incoming_link_count: 0, + reads: 1, + score: 0, + yours: true, + topic_id: 13, + topic_slug: "this-is-a-test-topic-for-polls", + display_username: null, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 2, + can_edit: true, + can_delete: false, + can_recover: true, + can_wiki: true, + read: true, + user_title: null, + actions_summary: [ + { id: 3, can_act: true }, + { id: 4, can_act: true }, + { id: 5, hidden: true, can_act: true }, + { id: 7, can_act: true }, + { id: 8, can_act: true } + ], + moderator: false, + admin: true, + staff: true, + user_id: 1, + hidden: false, + hidden_reason_id: null, + trust_level: 4, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + polls: { + poll: { + options: [ + { + id: "57ddd734344eb7436d64a7d68a0df444", + html: "test", + votes: 0 + }, + { + id: "b5b78d79ab5b5d75d4d33d8b87f5d2aa", + html: "haha", + votes: 0 + } + ], + voters: 2, + status: "open", + name: "poll" + }, + test: { + options: [ + { + id: "c26ad90783b0d80936e5fdb292b7963c", + html: "donkey", + votes: 0 + }, + { + id: "99f2b9ac452ba73b115fcf3556e6d2d4", + html: "kong", + votes: 0 + } + ], + voters: 3, + status: "open", + name: "test" + } + } + } + ], + stream: [19] + }, + timeline_lookup: [[1, 0]], + id: 13, + title: "This is a test topic for polls", + fancy_title: "This is a test topic for polls", + posts_count: 1, + created_at: "2016-12-01T02:39:48.055Z", + views: 1, + reply_count: 0, + participant_count: 1, + like_count: 0, + last_posted_at: "2016-12-01T02:39:49.199Z", + visible: true, + closed: false, + archived: false, + has_summary: false, + archetype: "regular", + slug: "this-is-a-test-topic-for-polls", + category_id: 1, + word_count: 10, + deleted_at: null, + user_id: 1, + draft: null, + draft_key: "topic_13", + draft_sequence: 4, + posted: true, + unpinned: null, + pinned_globally: false, + pinned: false, + pinned_at: null, + pinned_until: null, + details: { + auto_close_at: null, + auto_close_hours: null, + auto_close_based_on_last_post: false, + created_by: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + last_poster: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + participants: [ + { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png", + post_count: 1 + } + ], + suggested_topics: [ + { + id: 8, + title: "Welcome to Discourse", + fancy_title: "Welcome to Discourse", + slug: "welcome-to-discourse", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2016-11-24T02:10:54.328Z", + last_posted_at: "2016-11-24T02:10:54.393Z", + bumped: true, + bumped_at: "2016-11-24T02:10:54.393Z", + unseen: false, + pinned: true, + unpinned: null, + excerpt: + "The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + archetype: "regular", + like_count: 0, + views: 0, + category_id: 1, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: -1, + username: "system", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 12, + title: "Some testing topic testing", + fancy_title: "Some testing topic testing", + slug: "some-testing-topic-testing", + posts_count: 4, + reply_count: 0, + highest_post_number: 4, + image_url: null, + created_at: "2016-11-24T08:36:08.773Z", + last_posted_at: "2016-12-01T01:15:52.008Z", + bumped: true, + bumped_at: "2016-12-01T01:15:52.008Z", + unseen: false, + last_read_post_number: 4, + unread: 0, + new_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + liked: false, + archetype: "regular", + like_count: 0, + views: 2, + category_id: 1, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 11, + title: "Some testing topic", + fancy_title: "Some testing topic", + slug: "some-testing-topic", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2016-11-24T08:35:26.758Z", + last_posted_at: "2016-11-24T08:35:26.894Z", + bumped: true, + bumped_at: "2016-11-24T08:35:26.894Z", + unseen: false, + last_read_post_number: 1, + unread: 0, + new_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + liked: false, + archetype: "regular", + like_count: 0, + views: 0, + category_id: 1, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + } + } + ] + } + ], + notification_level: 3, + notifications_reason_id: 1, + can_move_posts: true, + can_edit: true, + can_delete: true, + can_recover: true, + can_remove_allowed_users: true, + can_invite_to: true, + can_create_post: true, + can_reply_as_new_topic: true, + can_flag_topic: true + }, + highest_post_number: 1, + last_read_post_number: 1, + last_read_post_id: 19, + deleted_by: null, + has_deleted: false, + actions_summary: [ + { id: 4, count: 0, hidden: false, can_act: true }, + { id: 7, count: 0, hidden: false, can_act: true }, + { id: 8, count: 0, hidden: false, can_act: true } + ], + chunk_size: 20, + bookmarked: false + } ]; }); visit("/t/this-is-a-test-topic-for-polls/13"); andThen(() => { - const polls = find('.poll'); + const polls = find(".poll"); - assert.equal(polls.length, 2, 'it should render the polls correctly'); - assert.equal(find('.info-number', polls[0]).text(), '2', 'it should display the right number of votes'); - assert.equal(find('.info-number', polls[1]).text(), '3', 'it should display the right number of votes'); + assert.equal(polls.length, 2, "it should render the polls correctly"); + assert.equal( + find(".info-number", polls[0]).text(), + "2", + "it should display the right number of votes" + ); + assert.equal( + find(".info-number", polls[1]).text(), + "3", + "it should display the right number of votes" + ); }); }); test("Public poll", assert => { - server.get('/t/12.json', () => { // eslint-disable-line no-undef + // prettier-ignore + server.get("/t/12.json", () => { // eslint-disable-line no-undef return [ 200, { "Content-Type": "application/json" }, - {"post_stream":{"posts":[{"id":15,"name":null,"username":"tgx","avatar_template":"/images/avatar.png","created_at":"2017-01-31T08:39:06.237Z","cooked":"
\n
\n
    \n
  • 1
  • \n
  • 2
  • \n
  • 3
  • \n
\n
\n

0voters

\n

Choose up to 3 options

\n

Votes are public.

\n
\n
\n
\nVote now!Show results\n
\n
","post_number":1,"post_type":1,"updated_at":"2017-01-31T08:39:06.237Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":12,"topic_slug":"this-is-a-topic-created-for-testing","display_username":null,"primary_group_name":null,"primary_group_flair_url":null,"primary_group_flair_bg_color":null,"primary_group_flair_color":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"can_wiki":true,"read":true,"user_title":null,"actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false,"polls":{"poll":{"options":[{"id":"4d8a15e3cc35750f016ce15a43937620","html":"1","votes":29},{"id":"cd314db7dfbac2b10687b6f39abfdf41","html":"2","votes":29},{"id":"68b434ff88aeae7054e42cd05a4d9056","html":"3","votes":42}],"voters":100,"status":"open","name":"poll","type":"multiple","min":"1","max":"3","public":"true"}}}],"stream":[15]},"timeline_lookup":[[1,0]],"id":12,"title":"This is a topic created for testing","fancy_title":"This is a topic created for testing","posts_count":1,"created_at":"2017-01-31T08:39:06.094Z","views":1,"reply_count":0,"participant_count":1,"like_count":0,"last_posted_at":"2017-01-31T08:39:06.237Z","visible":true,"closed":false,"archived":false,"has_summary":false,"archetype":"regular","slug":"this-is-a-topic-created-for-testing","category_id":1,"word_count":13,"deleted_at":null,"user_id":1,"draft":null,"draft_key":"topic_12","draft_sequence":1,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"pinned_until":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"last_poster":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"participants":[{"id":1,"username":"tgx","avatar_template":"/images/avatar.png","post_count":1,"primary_group_name":null,"primary_group_flair_url":null,"primary_group_flair_color":null,"primary_group_flair_bg_color":null}],"suggested_topics":[{"id":8,"title":"Welcome to Discourse","fancy_title":"Welcome to Discourse","slug":"welcome-to-discourse","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2017-01-31T07:53:45.363Z","last_posted_at":"2017-01-31T07:53:45.439Z","bumped":true,"bumped_at":"2017-01-31T07:53:45.439Z","unseen":false,"pinned":true,"unpinned":null,"excerpt":"The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …","visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":0,"category_id":1,"featured_link":null,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":-1,"username":"system","avatar_template":"/images/avatar.png"}}]},{"id":11,"title":"This is a test post to try out posts","fancy_title":"This is a test post to try out posts","slug":"this-is-a-test-post-to-try-out-posts","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2017-01-31T07:55:58.407Z","last_posted_at":"2017-01-31T07:55:58.634Z","bumped":true,"bumped_at":"2017-01-31T07:55:58.634Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":1,"category_id":1,"featured_link":null,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}]}],"notification_level":3,"notifications_reason_id":1,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":1,"last_read_post_number":1,"last_read_post_id":15,"deleted_by":null,"has_deleted":false,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false,"featured_link":null} + { + post_stream: { + posts: [ + { + id: 15, + name: null, + username: "tgx", + avatar_template: "/images/avatar.png", + created_at: "2017-01-31T08:39:06.237Z", + cooked: + '
\n
\n
    \n
  • 1
  • \n
  • 2
  • \n
  • 3
  • \n
\n
\n

0voters

\n

Choose up to 3 options

\n

Votes are public.

\n
\n
\n
\nVote now!Show results\n
\n
', + post_number: 1, + post_type: 1, + updated_at: "2017-01-31T08:39:06.237Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + avg_time: null, + incoming_link_count: 0, + reads: 1, + score: 0, + yours: true, + topic_id: 12, + topic_slug: "this-is-a-topic-created-for-testing", + display_username: null, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 1, + can_edit: true, + can_delete: false, + can_recover: true, + can_wiki: true, + read: true, + user_title: null, + actions_summary: [ + { id: 3, can_act: true }, + { id: 4, can_act: true }, + { id: 5, hidden: true, can_act: true }, + { id: 7, can_act: true }, + { id: 8, can_act: true } + ], + moderator: false, + admin: true, + staff: true, + user_id: 1, + hidden: false, + hidden_reason_id: null, + trust_level: 4, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + polls: { + poll: { + options: [ + { + id: "4d8a15e3cc35750f016ce15a43937620", + html: "1", + votes: 29 + }, + { + id: "cd314db7dfbac2b10687b6f39abfdf41", + html: "2", + votes: 29 + }, + { + id: "68b434ff88aeae7054e42cd05a4d9056", + html: "3", + votes: 42 + } + ], + voters: 100, + status: "open", + name: "poll", + type: "multiple", + min: "1", + max: "3", + public: "true" + } + } + } + ], + stream: [15] + }, + timeline_lookup: [[1, 0]], + id: 12, + title: "This is a topic created for testing", + fancy_title: "This is a topic created for testing", + posts_count: 1, + created_at: "2017-01-31T08:39:06.094Z", + views: 1, + reply_count: 0, + participant_count: 1, + like_count: 0, + last_posted_at: "2017-01-31T08:39:06.237Z", + visible: true, + closed: false, + archived: false, + has_summary: false, + archetype: "regular", + slug: "this-is-a-topic-created-for-testing", + category_id: 1, + word_count: 13, + deleted_at: null, + user_id: 1, + draft: null, + draft_key: "topic_12", + draft_sequence: 1, + posted: true, + unpinned: null, + pinned_globally: false, + pinned: false, + pinned_at: null, + pinned_until: null, + details: { + auto_close_at: null, + auto_close_hours: null, + auto_close_based_on_last_post: false, + created_by: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + last_poster: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + participants: [ + { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png", + post_count: 1, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_color: null, + primary_group_flair_bg_color: null + } + ], + suggested_topics: [ + { + id: 8, + title: "Welcome to Discourse", + fancy_title: "Welcome to Discourse", + slug: "welcome-to-discourse", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2017-01-31T07:53:45.363Z", + last_posted_at: "2017-01-31T07:53:45.439Z", + bumped: true, + bumped_at: "2017-01-31T07:53:45.439Z", + unseen: false, + pinned: true, + unpinned: null, + excerpt: + "The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + archetype: "regular", + like_count: 0, + views: 0, + category_id: 1, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: -1, + username: "system", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 11, + title: "This is a test post to try out posts", + fancy_title: "This is a test post to try out posts", + slug: "this-is-a-test-post-to-try-out-posts", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2017-01-31T07:55:58.407Z", + last_posted_at: "2017-01-31T07:55:58.634Z", + bumped: true, + bumped_at: "2017-01-31T07:55:58.634Z", + unseen: false, + last_read_post_number: 1, + unread: 0, + new_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + liked: false, + archetype: "regular", + like_count: 0, + views: 1, + category_id: 1, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + } + } + ] + } + ], + notification_level: 3, + notifications_reason_id: 1, + can_move_posts: true, + can_edit: true, + can_delete: true, + can_recover: true, + can_remove_allowed_users: true, + can_invite_to: true, + can_create_post: true, + can_reply_as_new_topic: true, + can_flag_topic: true + }, + highest_post_number: 1, + last_read_post_number: 1, + last_read_post_id: 15, + deleted_by: null, + has_deleted: false, + actions_summary: [ + { id: 4, count: 0, hidden: false, can_act: true }, + { id: 7, count: 0, hidden: false, can_act: true }, + { id: 8, count: 0, hidden: false, can_act: true } + ], + chunk_size: 20, + bookmarked: false, + featured_link: null + } ]; }); - server.get('/polls/voters.json', request => { // eslint-disable-line no-undef + // prettier-ignore + server.get("/polls/voters.json", request => { // eslint-disable-line no-undef let body = {}; if (_.isEqual(request.queryParams, { post_id: "15", poll_name: "poll" })) { - body = {"poll":{"68b434ff88aeae7054e42cd05a4d9056":[{"id":402,"username":"bruce400","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":409,"username":"bruce407","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":410,"username":"bruce408","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":411,"username":"bruce409","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":421,"username":"bruce419","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":422,"username":"bruce420","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":423,"username":"bruce421","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":426,"username":"bruce424","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":429,"username":"bruce427","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":437,"username":"bruce435","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":440,"username":"bruce438","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":442,"username":"bruce440","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":443,"username":"bruce441","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":445,"username":"bruce443","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":450,"username":"bruce448","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":451,"username":"bruce449","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":453,"username":"bruce451","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":455,"username":"bruce453","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":461,"username":"bruce459","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":466,"username":"bruce464","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":468,"username":"bruce466","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":477,"username":"bruce475","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":478,"username":"bruce476","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":498,"username":"bruce496","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":501,"username":"bruce499","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null}],"cd314db7dfbac2b10687b6f39abfdf41":[{"id":403,"username":"bruce401","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":404,"username":"bruce402","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":405,"username":"bruce403","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":408,"username":"bruce406","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":413,"username":"bruce411","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":414,"username":"bruce412","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":416,"username":"bruce414","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":418,"username":"bruce416","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":419,"username":"bruce417","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":433,"username":"bruce431","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":434,"username":"bruce432","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":435,"username":"bruce433","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":439,"username":"bruce437","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":441,"username":"bruce439","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":448,"username":"bruce446","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":449,"username":"bruce447","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":452,"username":"bruce450","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":462,"username":"bruce460","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":464,"username":"bruce462","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":465,"username":"bruce463","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":470,"username":"bruce468","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":471,"username":"bruce469","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":474,"username":"bruce472","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":476,"username":"bruce474","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":486,"username":"bruce484","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null}],"4d8a15e3cc35750f016ce15a43937620":[{"id":406,"username":"bruce404","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":407,"username":"bruce405","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":412,"username":"bruce410","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":415,"username":"bruce413","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":417,"username":"bruce415","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":420,"username":"bruce418","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":424,"username":"bruce422","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":425,"username":"bruce423","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":427,"username":"bruce425","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":428,"username":"bruce426","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":430,"username":"bruce428","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":431,"username":"bruce429","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":432,"username":"bruce430","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":436,"username":"bruce434","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":438,"username":"bruce436","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":444,"username":"bruce442","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":446,"username":"bruce444","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":447,"username":"bruce445","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":454,"username":"bruce452","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":458,"username":"bruce456","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":459,"username":"bruce457","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":481,"username":"bruce479","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":492,"username":"bruce490","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":494,"username":"bruce492","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":500,"username":"bruce498","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null}]}}; - } else if (_.isEqual(request.queryParams, { post_id: "15", poll_name: "poll", offset: "1", option_id: "68b434ff88aeae7054e42cd05a4d9056" })) { - body = {"poll":{"68b434ff88aeae7054e42cd05a4d9056":[{"id":402,"username":"bruce400","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":409,"username":"bruce407","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":410,"username":"bruce408","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":411,"username":"bruce409","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":421,"username":"bruce419","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":422,"username":"bruce420","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":423,"username":"bruce421","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":426,"username":"bruce424","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":429,"username":"bruce427","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":437,"username":"bruce435","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":440,"username":"bruce438","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":442,"username":"bruce440","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":443,"username":"bruce441","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":445,"username":"bruce443","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":450,"username":"bruce448","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":451,"username":"bruce449","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":453,"username":"bruce451","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":455,"username":"bruce453","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":456,"username":"bruce454","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":461,"username":"bruce459","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":466,"username":"bruce464","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":468,"username":"bruce466","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":477,"username":"bruce475","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":478,"username":"bruce476","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":498,"username":"bruce496","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null}]}}; + body = { + poll: { + "68b434ff88aeae7054e42cd05a4d9056": [ + { + id: 402, + username: "bruce400", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 409, + username: "bruce407", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 410, + username: "bruce408", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 411, + username: "bruce409", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 421, + username: "bruce419", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 422, + username: "bruce420", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 423, + username: "bruce421", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 426, + username: "bruce424", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 429, + username: "bruce427", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 437, + username: "bruce435", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 440, + username: "bruce438", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 442, + username: "bruce440", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 443, + username: "bruce441", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 445, + username: "bruce443", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 450, + username: "bruce448", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 451, + username: "bruce449", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 453, + username: "bruce451", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 455, + username: "bruce453", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 461, + username: "bruce459", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 466, + username: "bruce464", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 468, + username: "bruce466", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 477, + username: "bruce475", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 478, + username: "bruce476", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 498, + username: "bruce496", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 501, + username: "bruce499", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + } + ], + cd314db7dfbac2b10687b6f39abfdf41: [ + { + id: 403, + username: "bruce401", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 404, + username: "bruce402", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 405, + username: "bruce403", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 408, + username: "bruce406", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 413, + username: "bruce411", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 414, + username: "bruce412", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 416, + username: "bruce414", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 418, + username: "bruce416", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 419, + username: "bruce417", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 433, + username: "bruce431", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 434, + username: "bruce432", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 435, + username: "bruce433", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 439, + username: "bruce437", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 441, + username: "bruce439", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 448, + username: "bruce446", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 449, + username: "bruce447", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 452, + username: "bruce450", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 462, + username: "bruce460", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 464, + username: "bruce462", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 465, + username: "bruce463", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 470, + username: "bruce468", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 471, + username: "bruce469", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 474, + username: "bruce472", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 476, + username: "bruce474", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 486, + username: "bruce484", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + } + ], + "4d8a15e3cc35750f016ce15a43937620": [ + { + id: 406, + username: "bruce404", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 407, + username: "bruce405", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 412, + username: "bruce410", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 415, + username: "bruce413", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 417, + username: "bruce415", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 420, + username: "bruce418", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 424, + username: "bruce422", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 425, + username: "bruce423", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 427, + username: "bruce425", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 428, + username: "bruce426", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 430, + username: "bruce428", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 431, + username: "bruce429", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 432, + username: "bruce430", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 436, + username: "bruce434", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 438, + username: "bruce436", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 444, + username: "bruce442", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 446, + username: "bruce444", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 447, + username: "bruce445", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 454, + username: "bruce452", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 458, + username: "bruce456", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 459, + username: "bruce457", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 481, + username: "bruce479", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 492, + username: "bruce490", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 494, + username: "bruce492", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 500, + username: "bruce498", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + } + ] + } + }; + } else if ( + _.isEqual(request.queryParams, { + post_id: "15", + poll_name: "poll", + offset: "1", + option_id: "68b434ff88aeae7054e42cd05a4d9056" + }) + ) { + body = { + poll: { + "68b434ff88aeae7054e42cd05a4d9056": [ + { + id: 402, + username: "bruce400", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 409, + username: "bruce407", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 410, + username: "bruce408", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 411, + username: "bruce409", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 421, + username: "bruce419", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 422, + username: "bruce420", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 423, + username: "bruce421", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 426, + username: "bruce424", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 429, + username: "bruce427", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 437, + username: "bruce435", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 440, + username: "bruce438", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 442, + username: "bruce440", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 443, + username: "bruce441", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 445, + username: "bruce443", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 450, + username: "bruce448", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 451, + username: "bruce449", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 453, + username: "bruce451", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 455, + username: "bruce453", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 456, + username: "bruce454", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 461, + username: "bruce459", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 466, + username: "bruce464", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 468, + username: "bruce466", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 477, + username: "bruce475", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 478, + username: "bruce476", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 498, + username: "bruce496", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + } + ] + } + }; } return [200, { "Content-Type": "application/json" }, body]; @@ -53,45 +1331,686 @@ test("Public poll", assert => { visit("/t/this-is-a-topic-created-for-testing/12"); andThen(() => { - const polls = find('.poll'); - assert.equal(polls.length, 1, 'it should render the poll correctly'); + const polls = find(".poll"); + assert.equal(polls.length, 1, "it should render the poll correctly"); }); - click('button.toggle-results'); + click("button.toggle-results"); andThen(() => { assert.equal( - find('.poll-voters:first li').length, 25, - 'it should display the right number of voters' + find(".poll-voters:first li").length, + 25, + "it should display the right number of voters" ); }); - click('.poll-voters-toggle-expand:first a'); + click(".poll-voters-toggle-expand:first a"); andThen(() => { assert.equal( - find('.poll-voters:first li').length, 50, - 'it should display the right number of voters' + find(".poll-voters:first li").length, + 50, + "it should display the right number of voters" ); }); }); test("Public number poll", assert => { - server.get('/t/13.json', () => { // eslint-disable-line no-undef + // prettier-ignore + server.get("/t/13.json", () => { // eslint-disable-line no-undef return [ 200, { "Content-Type": "application/json" }, - {"post_stream":{"posts":[{"id":16,"name":null,"username":"tgx","avatar_template":"/images/avatar.png","created_at":"2017-01-31T09:11:11.281Z","cooked":"
\n
\n
    \n
  • 1
  • \n
  • 2
  • \n
  • 3
  • \n
  • 4
  • \n
  • 5
  • \n
  • 6
  • \n
  • 7
  • \n
  • 8
  • \n
  • 9
  • \n
  • 10
  • \n
  • 11
  • \n
  • 12
  • \n
  • 13
  • \n
  • 14
  • \n
  • 15
  • \n
  • 16
  • \n
  • 17
  • \n
  • 18
  • \n
  • 19
  • \n
  • 20
  • \n
\n
\n

0voters

\n

Votes are public.

\n
\n
\n
Show results
\n
","post_number":1,"post_type":1,"updated_at":"2017-01-31T09:11:11.281Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":13,"topic_slug":"this-is-a-topic-for-testing-number-poll","display_username":null,"primary_group_name":null,"primary_group_flair_url":null,"primary_group_flair_bg_color":null,"primary_group_flair_color":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"can_wiki":true,"read":true,"user_title":null,"actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false,"polls":{"poll":{"options":[{"id":"4d8a15e3cc35750f016ce15a43937620","html":"1","votes":2},{"id":"cd314db7dfbac2b10687b6f39abfdf41","html":"2","votes":1},{"id":"68b434ff88aeae7054e42cd05a4d9056","html":"3","votes":1},{"id":"aa2393b424f2f395abb63bf785760a3b","html":"4","votes":0},{"id":"8b2f2930cac0574c3450f5db9a6fb7f9","html":"5","votes":1},{"id":"60cad69e0cfcb3fa77a68d11d3758002","html":"6","votes":0},{"id":"9ab1070dec27185440cdabb4948a5e9a","html":"7","votes":1},{"id":"99944bf07088f815a966d585daed6a7e","html":"8","votes":3},{"id":"345a83050400d78f5fac98d381b45e23","html":"9","votes":3},{"id":"46c01f638a50d86e020f47469733b8be","html":"10","votes":3},{"id":"07f7f85b2a3809faff68a35e81a664eb","html":"11","votes":2},{"id":"b3e8c14e714910cb8dd7089f097be133","html":"12","votes":4},{"id":"b4f15431e07443c372d521e4ed131abe","html":"13","votes":2},{"id":"a77bc9a30933e5af327211db2da46e17","html":"14","votes":2},{"id":"303d7c623da1985e94a9d27d43596934","html":"15","votes":2},{"id":"4e885ead68ff4456f102843df9fbbd7f","html":"16","votes":1},{"id":"cbf6e2b72e403b12d7ee63a138f32647","html":"17","votes":2},{"id":"9364fa2d67fbd62c473165441ad69571","html":"18","votes":2},{"id":"eb8661f072794ea57baa7827cd8ffc88","html":"19","votes":1},{"id":"b373436e858c0821135f994a5ff3345f","html":"20","votes":2}],"voters":35,"status":"open","name":"poll","type":"number","min":"1","max":"20","step":"1","public":"true"}}}],"stream":[16]},"timeline_lookup":[[1,0]],"id":13,"title":"This is a topic for testing number poll","fancy_title":"This is a topic for testing number poll","posts_count":1,"created_at":"2017-01-31T09:11:11.161Z","views":1,"reply_count":0,"participant_count":1,"like_count":0,"last_posted_at":"2017-01-31T09:11:11.281Z","visible":true,"closed":false,"archived":false,"has_summary":false,"archetype":"regular","slug":"this-is-a-topic-for-testing-number-poll","category_id":1,"word_count":12,"deleted_at":null,"user_id":1,"draft":null,"draft_key":"topic_13","draft_sequence":1,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"pinned_until":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"last_poster":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"participants":[{"id":1,"username":"tgx","avatar_template":"/images/avatar.png","post_count":1,"primary_group_name":null,"primary_group_flair_url":null,"primary_group_flair_color":null,"primary_group_flair_bg_color":null}],"suggested_topics":[{"id":8,"title":"Welcome to Discourse","fancy_title":"Welcome to Discourse","slug":"welcome-to-discourse","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2017-01-31T07:53:45.363Z","last_posted_at":"2017-01-31T07:53:45.439Z","bumped":true,"bumped_at":"2017-01-31T07:53:45.439Z","unseen":false,"pinned":true,"unpinned":null,"excerpt":"The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …","visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":0,"category_id":1,"featured_link":null,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":-1,"username":"system","avatar_template":"/images/avatar.png"}}]},{"id":11,"title":"This is a test post to try out posts","fancy_title":"This is a test post to try out posts","slug":"this-is-a-test-post-to-try-out-posts","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2017-01-31T07:55:58.407Z","last_posted_at":"2017-01-31T07:55:58.634Z","bumped":true,"bumped_at":"2017-01-31T07:55:58.634Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":1,"category_id":1,"featured_link":null,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}]},{"id":12,"title":"This is a topic created for testing","fancy_title":"This is a topic created for testing","slug":"this-is-a-topic-created-for-testing","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2017-01-31T08:39:06.094Z","last_posted_at":"2017-01-31T08:39:06.237Z","bumped":true,"bumped_at":"2017-01-31T09:10:46.528Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":1,"category_id":1,"featured_link":null,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}]}],"notification_level":3,"notifications_reason_id":1,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":1,"last_read_post_number":1,"last_read_post_id":16,"deleted_by":null,"has_deleted":false,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false,"featured_link":null} + { + post_stream: { + posts: [ + { + id: 16, + name: null, + username: "tgx", + avatar_template: "/images/avatar.png", + created_at: "2017-01-31T09:11:11.281Z", + cooked: + '
\n
\n
    \n
  • 1
  • \n
  • 2
  • \n
  • 3
  • \n
  • 4
  • \n
  • 5
  • \n
  • 6
  • \n
  • 7
  • \n
  • 8
  • \n
  • 9
  • \n
  • 10
  • \n
  • 11
  • \n
  • 12
  • \n
  • 13
  • \n
  • 14
  • \n
  • 15
  • \n
  • 16
  • \n
  • 17
  • \n
  • 18
  • \n
  • 19
  • \n
  • 20
  • \n
\n
\n

0voters

\n

Votes are public.

\n
\n
\n
Show results
\n
', + post_number: 1, + post_type: 1, + updated_at: "2017-01-31T09:11:11.281Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + avg_time: null, + incoming_link_count: 0, + reads: 1, + score: 0, + yours: true, + topic_id: 13, + topic_slug: "this-is-a-topic-for-testing-number-poll", + display_username: null, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + version: 1, + can_edit: true, + can_delete: false, + can_recover: true, + can_wiki: true, + read: true, + user_title: null, + actions_summary: [ + { id: 3, can_act: true }, + { id: 4, can_act: true }, + { id: 5, hidden: true, can_act: true }, + { id: 7, can_act: true }, + { id: 8, can_act: true } + ], + moderator: false, + admin: true, + staff: true, + user_id: 1, + hidden: false, + hidden_reason_id: null, + trust_level: 4, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false, + polls: { + poll: { + options: [ + { + id: "4d8a15e3cc35750f016ce15a43937620", + html: "1", + votes: 2 + }, + { + id: "cd314db7dfbac2b10687b6f39abfdf41", + html: "2", + votes: 1 + }, + { + id: "68b434ff88aeae7054e42cd05a4d9056", + html: "3", + votes: 1 + }, + { + id: "aa2393b424f2f395abb63bf785760a3b", + html: "4", + votes: 0 + }, + { + id: "8b2f2930cac0574c3450f5db9a6fb7f9", + html: "5", + votes: 1 + }, + { + id: "60cad69e0cfcb3fa77a68d11d3758002", + html: "6", + votes: 0 + }, + { + id: "9ab1070dec27185440cdabb4948a5e9a", + html: "7", + votes: 1 + }, + { + id: "99944bf07088f815a966d585daed6a7e", + html: "8", + votes: 3 + }, + { + id: "345a83050400d78f5fac98d381b45e23", + html: "9", + votes: 3 + }, + { + id: "46c01f638a50d86e020f47469733b8be", + html: "10", + votes: 3 + }, + { + id: "07f7f85b2a3809faff68a35e81a664eb", + html: "11", + votes: 2 + }, + { + id: "b3e8c14e714910cb8dd7089f097be133", + html: "12", + votes: 4 + }, + { + id: "b4f15431e07443c372d521e4ed131abe", + html: "13", + votes: 2 + }, + { + id: "a77bc9a30933e5af327211db2da46e17", + html: "14", + votes: 2 + }, + { + id: "303d7c623da1985e94a9d27d43596934", + html: "15", + votes: 2 + }, + { + id: "4e885ead68ff4456f102843df9fbbd7f", + html: "16", + votes: 1 + }, + { + id: "cbf6e2b72e403b12d7ee63a138f32647", + html: "17", + votes: 2 + }, + { + id: "9364fa2d67fbd62c473165441ad69571", + html: "18", + votes: 2 + }, + { + id: "eb8661f072794ea57baa7827cd8ffc88", + html: "19", + votes: 1 + }, + { + id: "b373436e858c0821135f994a5ff3345f", + html: "20", + votes: 2 + } + ], + voters: 35, + status: "open", + name: "poll", + type: "number", + min: "1", + max: "20", + step: "1", + public: "true" + } + } + } + ], + stream: [16] + }, + timeline_lookup: [[1, 0]], + id: 13, + title: "This is a topic for testing number poll", + fancy_title: "This is a topic for testing number poll", + posts_count: 1, + created_at: "2017-01-31T09:11:11.161Z", + views: 1, + reply_count: 0, + participant_count: 1, + like_count: 0, + last_posted_at: "2017-01-31T09:11:11.281Z", + visible: true, + closed: false, + archived: false, + has_summary: false, + archetype: "regular", + slug: "this-is-a-topic-for-testing-number-poll", + category_id: 1, + word_count: 12, + deleted_at: null, + user_id: 1, + draft: null, + draft_key: "topic_13", + draft_sequence: 1, + posted: true, + unpinned: null, + pinned_globally: false, + pinned: false, + pinned_at: null, + pinned_until: null, + details: { + auto_close_at: null, + auto_close_hours: null, + auto_close_based_on_last_post: false, + created_by: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + last_poster: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + participants: [ + { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png", + post_count: 1, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_color: null, + primary_group_flair_bg_color: null + } + ], + suggested_topics: [ + { + id: 8, + title: "Welcome to Discourse", + fancy_title: "Welcome to Discourse", + slug: "welcome-to-discourse", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2017-01-31T07:53:45.363Z", + last_posted_at: "2017-01-31T07:53:45.439Z", + bumped: true, + bumped_at: "2017-01-31T07:53:45.439Z", + unseen: false, + pinned: true, + unpinned: null, + excerpt: + "The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + archetype: "regular", + like_count: 0, + views: 0, + category_id: 1, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: -1, + username: "system", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 11, + title: "This is a test post to try out posts", + fancy_title: "This is a test post to try out posts", + slug: "this-is-a-test-post-to-try-out-posts", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2017-01-31T07:55:58.407Z", + last_posted_at: "2017-01-31T07:55:58.634Z", + bumped: true, + bumped_at: "2017-01-31T07:55:58.634Z", + unseen: false, + last_read_post_number: 1, + unread: 0, + new_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + liked: false, + archetype: "regular", + like_count: 0, + views: 1, + category_id: 1, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 12, + title: "This is a topic created for testing", + fancy_title: "This is a topic created for testing", + slug: "this-is-a-topic-created-for-testing", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2017-01-31T08:39:06.094Z", + last_posted_at: "2017-01-31T08:39:06.237Z", + bumped: true, + bumped_at: "2017-01-31T09:10:46.528Z", + unseen: false, + last_read_post_number: 1, + unread: 0, + new_posts: 0, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + notification_level: 3, + bookmarked: false, + liked: false, + archetype: "regular", + like_count: 0, + views: 1, + category_id: 1, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + } + } + ] + } + ], + notification_level: 3, + notifications_reason_id: 1, + can_move_posts: true, + can_edit: true, + can_delete: true, + can_recover: true, + can_remove_allowed_users: true, + can_invite_to: true, + can_create_post: true, + can_reply_as_new_topic: true, + can_flag_topic: true + }, + highest_post_number: 1, + last_read_post_number: 1, + last_read_post_id: 16, + deleted_by: null, + has_deleted: false, + actions_summary: [ + { id: 4, count: 0, hidden: false, can_act: true }, + { id: 7, count: 0, hidden: false, can_act: true }, + { id: 8, count: 0, hidden: false, can_act: true } + ], + chunk_size: 20, + bookmarked: false, + featured_link: null + } ]; }); - server.get('/polls/voters.json', request => { // eslint-disable-line no-undef + // prettier-ignore + server.get("/polls/voters.json", request => { // eslint-disable-line no-undef let body = {}; if (_.isEqual(request.queryParams, { post_id: "16", poll_name: "poll" })) { - body = {"poll":[{"id":402,"username":"bruce400","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":403,"username":"bruce401","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":404,"username":"bruce402","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":405,"username":"bruce403","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":406,"username":"bruce404","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":407,"username":"bruce405","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":408,"username":"bruce406","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":409,"username":"bruce407","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":410,"username":"bruce408","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":411,"username":"bruce409","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":412,"username":"bruce410","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":413,"username":"bruce411","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":414,"username":"bruce412","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":415,"username":"bruce413","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":416,"username":"bruce414","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":417,"username":"bruce415","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":419,"username":"bruce417","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":421,"username":"bruce419","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":422,"username":"bruce420","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":424,"username":"bruce422","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":425,"username":"bruce423","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":427,"username":"bruce425","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":430,"username":"bruce428","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":431,"username":"bruce429","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":435,"username":"bruce433","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null}]}; - } else if (_.isEqual(request.queryParams, { post_id: "16", poll_name: "poll", offset: "1" })) { - body = {"poll":[{"id":418,"username":"bruce416","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":420,"username":"bruce418","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":423,"username":"bruce421","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":426,"username":"bruce424","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":428,"username":"bruce426","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":429,"username":"bruce427","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":432,"username":"bruce430","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":433,"username":"bruce431","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":434,"username":"bruce432","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null},{"id":436,"username":"bruce434","avatar_template":"/images/avatar.png","name":"Bruce Wayne","title":null}]}; + body = { + poll: [ + { + id: 402, + username: "bruce400", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 403, + username: "bruce401", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 404, + username: "bruce402", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 405, + username: "bruce403", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 406, + username: "bruce404", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 407, + username: "bruce405", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 408, + username: "bruce406", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 409, + username: "bruce407", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 410, + username: "bruce408", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 411, + username: "bruce409", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 412, + username: "bruce410", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 413, + username: "bruce411", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 414, + username: "bruce412", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 415, + username: "bruce413", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 416, + username: "bruce414", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 417, + username: "bruce415", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 419, + username: "bruce417", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 421, + username: "bruce419", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 422, + username: "bruce420", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 424, + username: "bruce422", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 425, + username: "bruce423", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 427, + username: "bruce425", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 430, + username: "bruce428", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 431, + username: "bruce429", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 435, + username: "bruce433", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + } + ] + }; + } else if ( + _.isEqual(request.queryParams, { + post_id: "16", + poll_name: "poll", + offset: "1" + }) + ) { + body = { + poll: [ + { + id: 418, + username: "bruce416", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 420, + username: "bruce418", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 423, + username: "bruce421", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 426, + username: "bruce424", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 428, + username: "bruce426", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 429, + username: "bruce427", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 432, + username: "bruce430", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 433, + username: "bruce431", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 434, + username: "bruce432", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + }, + { + id: 436, + username: "bruce434", + avatar_template: "/images/avatar.png", + name: "Bruce Wayne", + title: null + } + ] + }; } return [200, { "Content-Type": "application/json" }, body]; @@ -100,25 +2019,27 @@ test("Public number poll", assert => { visit("/t/this-is-a-topic-for-testing-number-poll/13"); andThen(() => { - const polls = find('.poll'); - assert.equal(polls.length, 1, 'it should render the poll correctly'); + const polls = find(".poll"); + assert.equal(polls.length, 1, "it should render the poll correctly"); }); - click('button.toggle-results'); + click("button.toggle-results"); andThen(() => { assert.equal( - find('.poll-voters:first li').length, 25, - 'it should display the right number of voters' + find(".poll-voters:first li").length, + 25, + "it should display the right number of voters" ); }); - click('.poll-voters-toggle-expand:first a'); + click(".poll-voters-toggle-expand:first a"); andThen(() => { assert.equal( - find('.poll-voters:first li').length, 35, - 'it should display the right number of voters' + find(".poll-voters:first li").length, + 35, + "it should display the right number of voters" ); }); }); diff --git a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 index babab76ae7..d80e7c54f2 100644 --- a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 +++ b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 @@ -1,13 +1,13 @@ -import { mapRoutes } from 'discourse/mapping-router'; +import { mapRoutes } from "discourse/mapping-router"; moduleFor("controller:poll-ui-builder", "controller:poll-ui-builder", { setup() { - this.registry.register('router:main', mapRoutes()); - this.subject().set('toolbarEvent', { + this.registry.register("router:main", mapRoutes()); + this.subject().set("toolbarEvent", { getText: () => "" }); }, - needs: ['controller:modal'] + needs: ["controller:modal"] }); test("isMultiple", function(assert) { @@ -91,46 +91,73 @@ test("pollMinOptions", function(assert) { pollOptionsCount: 1 }); - assert.deepEqual(controller.get("pollMinOptions"), [{ name: 1, value: 1 }], "it should return the right options"); + assert.deepEqual( + controller.get("pollMinOptions"), + [{ name: 1, value: 1 }], + "it should return the right options" + ); controller.set("pollOptionsCount", 2); - assert.deepEqual(controller.get("pollMinOptions"), [ - { name: 1, value: 1 }, { name: 2, value: 2 } - ], "it should return the right options"); + assert.deepEqual( + controller.get("pollMinOptions"), + [{ name: 1, value: 1 }, { name: 2, value: 2 }], + "it should return the right options" + ); controller.set("isNumber", true); controller.siteSettings.poll_maximum_options = 2; - assert.deepEqual(controller.get("pollMinOptions"), [ - { name: 1, value: 1 }, { name: 2, value: 2 } - ], "it should return the right options"); + assert.deepEqual( + controller.get("pollMinOptions"), + [{ name: 1, value: 1 }, { name: 2, value: 2 }], + "it should return the right options" + ); }); test("pollMaxOptions", function(assert) { const controller = this.subject(); controller.siteSettings = Discourse.SiteSettings; - controller.setProperties({ isMultiple: true, pollOptionsCount: 1, pollMin: 1 }); + controller.setProperties({ + isMultiple: true, + pollOptionsCount: 1, + pollMin: 1 + }); - assert.deepEqual(controller.get("pollMaxOptions"), [], "it should return the right options"); + assert.deepEqual( + controller.get("pollMaxOptions"), + [], + "it should return the right options" + ); controller.set("pollOptionsCount", 2); - assert.deepEqual(controller.get("pollMaxOptions"), [ - { name: 2, value: 2 } - ], "it should return the right options"); + assert.deepEqual( + controller.get("pollMaxOptions"), + [{ name: 2, value: 2 }], + "it should return the right options" + ); controller.siteSettings.poll_maximum_options = 3; - controller.setProperties({ isMultiple: false, isNumber: true, pollStep: 2, pollMin: 1 }); + controller.setProperties({ + isMultiple: false, + isNumber: true, + pollStep: 2, + pollMin: 1 + }); - assert.deepEqual(controller.get("pollMaxOptions"), [ - { name: 2, value: 2 }, - { name: 3, value: 3 }, - { name: 4, value: 4 }, - { name: 5, value: 5 }, - { name: 6, value: 6 } - ], "it should return the right options"); + assert.deepEqual( + controller.get("pollMaxOptions"), + [ + { name: 2, value: 2 }, + { name: 3, value: 3 }, + { name: 4, value: 4 }, + { name: 5, value: 5 }, + { name: 6, value: 6 } + ], + "it should return the right options" + ); }); test("pollStepOptions", function(assert) { @@ -140,15 +167,19 @@ test("pollStepOptions", function(assert) { controller.set("isNumber", false); - assert.equal(controller.get("pollStepOptions"), null, "is should return null"); + assert.equal( + controller.get("pollStepOptions"), + null, + "is should return null" + ); controller.setProperties({ isNumber: true }); - assert.deepEqual(controller.get("pollStepOptions"), [ - { name: 1, value: 1 }, - { name: 2, value: 2 }, - { name: 3, value: 3 } - ], "it should return the right options"); + assert.deepEqual( + controller.get("pollStepOptions"), + [{ name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }], + "it should return the right options" + ); }); test("disableInsert", function(assert) { @@ -187,19 +218,35 @@ test("number pollOutput", function(assert) { pollMin: 1 }); - assert.equal(controller.get("pollOutput"), "[poll type=number min=1 max=20 step=1]\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=number min=1 max=20 step=1]\n[/poll]", + "it should return the right output" + ); controller.set("pollStep", 2); - assert.equal(controller.get("pollOutput"), "[poll type=number min=1 max=20 step=2]\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=number min=1 max=20 step=2]\n[/poll]", + "it should return the right output" + ); controller.set("publicPoll", true); - assert.equal(controller.get("pollOutput"), "[poll type=number min=1 max=20 step=2 public=true]\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=number min=1 max=20 step=2 public=true]\n[/poll]", + "it should return the right output" + ); controller.set("pollStep", 0); - assert.equal(controller.get("pollOutput"), "[poll type=number min=1 max=20 step=1 public=true]\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=number min=1 max=20 step=1 public=true]\n[/poll]", + "it should return the right output" + ); }); test("regular pollOutput", function(assert) { @@ -213,14 +260,21 @@ test("regular pollOutput", function(assert) { pollType: controller.get("regularPollType") }); - assert.equal(controller.get("pollOutput"), "[poll type=regular]\n* 1\n* 2\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=regular]\n* 1\n* 2\n[/poll]", + "it should return the right output" + ); controller.set("publicPoll", "true"); - assert.equal(controller.get("pollOutput"), "[poll type=regular public=true]\n* 1\n* 2\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=regular public=true]\n* 1\n* 2\n[/poll]", + "it should return the right output" + ); }); - test("multiple pollOutput", function(assert) { const controller = this.subject(); controller.siteSettings = Discourse.SiteSettings; @@ -233,9 +287,17 @@ test("multiple pollOutput", function(assert) { pollOptions: "\n\n1\n\n2" }); - assert.equal(controller.get("pollOutput"), "[poll type=multiple min=1 max=2]\n* 1\n* 2\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=multiple min=1 max=2]\n* 1\n* 2\n[/poll]", + "it should return the right output" + ); controller.set("publicPoll", "true"); - assert.equal(controller.get("pollOutput"), "[poll type=multiple min=1 max=2 public=true]\n* 1\n* 2\n[/poll]", "it should return the right output"); + assert.equal( + controller.get("pollOutput"), + "[poll type=multiple min=1 max=2 public=true]\n* 1\n* 2\n[/poll]", + "it should return the right output" + ); }); diff --git a/plugins/poll/test/javascripts/widgets/discourse-poll-option-test.js.es6 b/plugins/poll/test/javascripts/widgets/discourse-poll-option-test.js.es6 index 75035b6651..751607e373 100644 --- a/plugins/poll/test/javascripts/widgets/discourse-poll-option-test.js.es6 +++ b/plugins/poll/test/javascripts/widgets/discourse-poll-option-test.js.es6 @@ -1,64 +1,64 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; -moduleForWidget('discourse-poll-option'); +import { moduleForWidget, widgetTest } from "helpers/widget-test"; +moduleForWidget("discourse-poll-option"); const template = `{{mount-widget widget="discourse-poll-option" args=(hash option=option isMultiple=isMultiple vote=vote)}}`; -widgetTest('single, not selected', { +widgetTest("single, not selected", { template, beforeEach() { - this.set('option', { id: 'opt-id' }); - this.set('vote', []); + this.set("option", { id: "opt-id" }); + this.set("vote", []); }, test(assert) { - assert.ok(find('li .d-icon-circle-o:eq(0)').length === 1); + assert.ok(find("li .d-icon-circle-o:eq(0)").length === 1); } }); -widgetTest('single, selected', { +widgetTest("single, selected", { template, beforeEach() { - this.set('option', { id: 'opt-id' }); - this.set('vote', ['opt-id']); + this.set("option", { id: "opt-id" }); + this.set("vote", ["opt-id"]); }, test(assert) { - assert.ok(find('li .d-icon-dot-circle-o:eq(0)').length === 1); + assert.ok(find("li .d-icon-dot-circle-o:eq(0)").length === 1); } }); -widgetTest('multi, not selected', { +widgetTest("multi, not selected", { template, beforeEach() { this.setProperties({ - option: { id: 'opt-id' }, + option: { id: "opt-id" }, isMultiple: true, vote: [] }); }, test(assert) { - assert.ok(find('li .d-icon-square-o:eq(0)').length === 1); + assert.ok(find("li .d-icon-square-o:eq(0)").length === 1); } }); -widgetTest('multi, selected', { +widgetTest("multi, selected", { template, beforeEach() { this.setProperties({ - option: { id: 'opt-id' }, + option: { id: "opt-id" }, isMultiple: true, - vote: ['opt-id'] + vote: ["opt-id"] }); }, test(assert) { - assert.ok(find('li .d-icon-check-square-o:eq(0)').length === 1); + assert.ok(find("li .d-icon-check-square-o:eq(0)").length === 1); } }); diff --git a/plugins/poll/test/javascripts/widgets/discourse-poll-standard-results-test.js.es6 b/plugins/poll/test/javascripts/widgets/discourse-poll-standard-results-test.js.es6 index d289f45ae8..bfcd7be01d 100644 --- a/plugins/poll/test/javascripts/widgets/discourse-poll-standard-results-test.js.es6 +++ b/plugins/poll/test/javascripts/widgets/discourse-poll-standard-results-test.js.es6 @@ -1,67 +1,76 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; -moduleForWidget('discourse-poll-standard-results'); +import { moduleForWidget, widgetTest } from "helpers/widget-test"; +moduleForWidget("discourse-poll-standard-results"); const template = `{{mount-widget widget="discourse-poll-standard-results" args=(hash poll=poll isMultiple=isMultiple)}}`; -widgetTest('options in descending order', { +widgetTest("options in descending order", { template, beforeEach() { - this.set('poll', Ember.Object.create({ - options: [{ votes: 5 }, { votes: 4 }], - voters: 9 - })); + this.set( + "poll", + Ember.Object.create({ + options: [{ votes: 5 }, { votes: 4 }], + voters: 9 + }) + ); }, test(assert) { - assert.equal(this.$('.option .percentage:eq(0)').text(), '56%'); - assert.equal(this.$('.option .percentage:eq(1)').text(), '44%'); + assert.equal(this.$(".option .percentage:eq(0)").text(), "56%"); + assert.equal(this.$(".option .percentage:eq(1)").text(), "44%"); } }); -widgetTest('options in ascending order', { +widgetTest("options in ascending order", { template, beforeEach() { - this.set('poll', Ember.Object.create({ - options: [{ votes: 4 }, { votes: 5 }], - voters: 9 - })); + this.set( + "poll", + Ember.Object.create({ + options: [{ votes: 4 }, { votes: 5 }], + voters: 9 + }) + ); }, test(assert) { - assert.equal(this.$('.option .percentage:eq(0)').text(), '56%'); - assert.equal(this.$('.option .percentage:eq(1)').text(), '44%'); + assert.equal(this.$(".option .percentage:eq(0)").text(), "56%"); + assert.equal(this.$(".option .percentage:eq(1)").text(), "44%"); } }); -widgetTest('multiple options in descending order', { +widgetTest("multiple options in descending order", { template, beforeEach() { - this.set('isMultiple', true); - this.set('poll', Ember.Object.create({ - type: 'multiple', - options: [ - { votes: 5, html: 'a' }, - { votes: 2, html: 'b' }, - { votes: 4, html: 'c' }, - { votes: 1, html: 'b' }, - { votes: 1, html: 'a' } - ], - voters: 12 - })); + this.set("isMultiple", true); + this.set( + "poll", + Ember.Object.create({ + type: "multiple", + options: [ + { votes: 5, html: "a" }, + { votes: 2, html: "b" }, + { votes: 4, html: "c" }, + { votes: 1, html: "b" }, + { votes: 1, html: "a" } + ], + voters: 12 + }) + ); }, test(assert) { - assert.equal(this.$('.option .percentage:eq(0)').text(), '41%'); - assert.equal(this.$('.option .percentage:eq(1)').text(), '33%'); - assert.equal(this.$('.option .percentage:eq(2)').text(), '16%'); - assert.equal(this.$('.option .percentage:eq(3)').text(), '8%'); - assert.equal(this.$('.option span:nth-child(2):eq(3)').text(), 'a'); - assert.equal(this.$('.option .percentage:eq(4)').text(), '8%'); - assert.equal(this.$('.option span:nth-child(2):eq(4)').text(), 'b'); + assert.equal(this.$(".option .percentage:eq(0)").text(), "41%"); + assert.equal(this.$(".option .percentage:eq(1)").text(), "33%"); + assert.equal(this.$(".option .percentage:eq(2)").text(), "16%"); + assert.equal(this.$(".option .percentage:eq(3)").text(), "8%"); + assert.equal(this.$(".option span:nth-child(2):eq(3)").text(), "a"); + assert.equal(this.$(".option .percentage:eq(4)").text(), "8%"); + assert.equal(this.$(".option span:nth-child(2):eq(4)").text(), "b"); } }); diff --git a/script/bulk_import/discourse_merger.rb b/script/bulk_import/discourse_merger.rb new file mode 100644 index 0000000000..c8498ca6aa --- /dev/null +++ b/script/bulk_import/discourse_merger.rb @@ -0,0 +1,623 @@ +require_relative "base" + +class BulkImport::DiscourseMerger < BulkImport::Base + + NOW ||= "now()".freeze + CUSTOM_FIELDS = ['category', 'group', 'post', 'topic', 'user'] + + # DB_NAME: name of database being merged into the current local db + # DB_HOST: hostname of database being merged + # UPLOADS_PATH: absolute path of the directory containing "original" + # and "optimized" dirs. e.g. /home/discourse/other-site/public/uploads/default + # SOURCE_BASE_URL: base url of the site being merged. e.g. https://meta.discourse.org + # SOURCE_CDN: (optional) base url of the CDN of the site being merged. + # e.g. https://discourse-cdn-sjc1.com/business4 + + def initialize + local_db = ActiveRecord::Base.connection_config + @raw_connection = PG.connect(dbname: local_db[:database], host: 'localhost', port: local_db[:port]) + + @source_db_config = { + dbname: ENV["DB_NAME"] || 'dd_demo', + host: ENV["DB_HOST"] || 'localhost' + } + + raise "SOURCE_BASE_URL missing!" unless ENV['SOURCE_BASE_URL'] + + @source_base_url = ENV["SOURCE_BASE_URL"] + @uploads_path = ENV['UPLOADS_PATH'] + @uploader = ImportScripts::Uploader.new + + if ENV['SOURCE_CDN'] + @source_cdn = ENV['SOURCE_CDN'] + end + + local_version = @raw_connection.exec("select max(version) from schema_migrations") + local_version = local_version.first['max'] + source_version = source_raw_connection.exec("select max(version) from schema_migrations") + source_version = source_version.first['max'] + + if local_version != source_version + raise "DB schema mismatch. Databases must be at the same migration version. Local is #{local_version}, other is #{source_version}" + end + + @encoder = PG::TextEncoder::CopyRow.new + + @merged_user_ids = [] + @tags = {} + @tag_groups = {} + @uploads = {} + @post_actions = {} + @notifications = {} + @badge_groupings = {} + @badges = {} + + @auto_group_ids = Group::AUTO_GROUPS.values + + # add your authorized extensions here: + SiteSetting.authorized_extensions = ['jpg', 'jpeg', 'png', 'gif'].join('|') + + @sequences = {} + end + + def start + run + ensure + @raw_connection&.close + source_raw_connection&.close + end + + def execute + copy_users + copy_user_stuff + copy_groups + copy_categories + copy_topics + copy_posts + copy_tags + copy_uploads if @uploads_path + copy_everything_else + copy_badges + fix_category_descriptions + + # TODO: update @mentions for usernames changed due to conflict + end + + def source_raw_connection + @source_raw_connection ||= PG.connect(@source_db_config) + end + + def copy_users + puts '', "merging users..." + + imported_ids = [] + + usernames_lower = User.unscoped.pluck(:username_lower).to_set + + columns = User.columns.map(&:name) + sql = "COPY users (#{columns.map { |c| "\"#{c}\"" }.join(",")}) FROM STDIN" + + @raw_connection.copy_data(sql, @encoder) do + source_raw_connection.exec("SELECT #{columns.map { |c| "u.\"#{c}\"" }.join(",")}, e.email FROM users u INNER JOIN user_emails e ON (u.id = e.user_id AND e.primary = TRUE) WHERE u.id > 0").each do |row| + old_user_id = row['id']&.to_i + if existing = UserEmail.where(email: row.delete('email')).first&.user + # Merge these users + @users[old_user_id.to_s] = existing.id + @merged_user_ids << old_user_id + next + else + # New user + unless usernames_lower.add?(row['username_lower']) + username = row['username'] + "_1" + username.next! until usernames_lower.add?(username.downcase) + row['username'] = username + row['username_lower'] = row['username'].downcase + end + + row['id'] = (@last_user_id += 1) + @users[old_user_id.to_s] = row['id'] + @raw_connection.put_copy_data row.values + end + imported_ids << old_user_id + end + end + + @sequences[User.sequence_name] = @last_user_id + 1 if @last_user_id + + create_custom_fields('user', 'id', imported_ids) do |old_user_id| + { value: old_user_id, record_id: user_id_from_imported_id(old_user_id) } + end + end + + def copy_user_stuff + [ + UserEmail, EmailToken, UserStat, UserOption, UserProfile, EmailChangeRequest, + UserVisit, UserSearchData, GivenDailyLike, UserSecondFactor, UserOpenId + ].each do |c| + copy_model(c, skip_if_merged: true, is_a_user_model: true, skip_processing: true) + end + + [FacebookUserInfo, GithubUserInfo, GoogleUserInfo, InstagramUserInfo, Oauth2UserInfo, + SingleSignOnRecord, TwitterUserInfo + ].each do |c| + copy_model(c, skip_if_merged: true, is_a_user_model: true) + end + end + + def copy_groups + copy_model(Group, + mapping: @groups, + skip_processing: true, + select_sql: "SELECT #{Group.columns.map { |c| "\"#{c.name}\"" }.join(', ')} FROM groups WHERE automatic = false" + ) + + copy_model(GroupUser, skip_if_merged: true) + copy_model(GroupArchivedMessage) + copy_model(GroupMention, skip_processing: true) + + [CategoryGroup, GroupHistory].each do |k| + col_list = k.columns.map { |c| "\"#{c.name}\"" }.join(', ') + copy_model(k, + select_sql: "SELECT #{col_list} FROM #{k.table_name} WHERE group_id NOT IN (#{@auto_group_ids.join(', ')})" + ) + end + end + + def copy_categories + puts "merging categories..." + + columns = Category.columns.map(&:name) + imported_ids = [] + last_id = Category.unscoped.maximum(:id) + + sql = "COPY categories (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + @raw_connection.copy_data(sql, @encoder) do + source_raw_connection.exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM categories").each do |row| + + if existing = Category.where(name: row['name']).first + @categories[row['id']] = existing.id + next + end + + old_user_id = row['user_id'].to_i + if old_user_id >= 1 + row['user_id'] = user_id_from_imported_id(old_user_id) || -1 + end + + if row['parent_category_id'] + row['parent_category_id'] = category_id_from_imported_id(row['parent_category_id']) + end + + old_id = row['id'] + row['id'] = (last_id += 1) + imported_ids << old_id + @categories[old_id.to_s] = row['id'] + + @raw_connection.put_copy_data(row.values) + end + end + + @sequences[Category.sequence_name] = last_id + 1 + + create_custom_fields('category', 'id', imported_ids) do |imported_id| + { + record_id: category_id_from_imported_id(imported_id), + value: imported_id, + } + end + end + + def fix_category_descriptions + puts 'updating category description topic ids...' + + @categories.each do |old_id, new_id| + category = Category.find(new_id) + if description_topic_id = topic_id_from_imported_id(category.topic_id) + category.topic_id = description_topic_id + category.save! + end + end + end + + def copy_topics + copy_model(Topic, skip_processing: true, mapping: @topics) + [TopicAllowedGroup, TopicAllowedUser, TopicEmbed, TopicSearchData, + TopicTimer, TopicUser, TopicViewItem + ].each do |k| + copy_model(k, skip_processing: true) + end + end + + def copy_posts + copy_model(Post, skip_processing: true, mapping: @posts) + copy_model(PostAction, mapping: @post_actions) + [PostReply, TopicLink, UserAction, QuotedPost].each do |k| + copy_model(k) + end + [PostStat, IncomingEmail, PostDetail, PostRevision].each do |k| + copy_model(k, skip_processing: true) + end + end + + def copy_tags + puts "merging tags..." + + columns = Tag.columns.map(&:name) + imported_ids = [] + last_id = Tag.unscoped.maximum(:id) + + sql = "COPY tags (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + @raw_connection.copy_data(sql, @encoder) do + source_raw_connection.exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM tags").each do |row| + + if existing = Tag.where(name: row['name']).first + @tags[row['id']] = existing.id + next + end + + old_id = row['id'] + row['id'] = (last_id += 1) + @tags[old_id.to_s] = row['id'] + + @raw_connection.put_copy_data(row.values) + end + end + + @sequences[Tag.sequence_name] = last_id + 1 + + [TagUser, TopicTag, CategoryTag, CategoryTagStat].each do |k| + copy_model(k, skip_processing: true) + end + copy_model(TagGroup, mapping: @tag_groups) + [TagGroupMembership, CategoryTagGroup].each do |k| + copy_model(k, skip_processing: true) + end + + col_list = TagGroupPermission.columns.map { |c| "\"#{c.name}\"" }.join(', ') + copy_model(TagGroupPermission, + skip_processing: true, + select_sql: "SELECT #{col_list} FROM tag_group_permissions WHERE group_id NOT IN (#{@auto_group_ids.join(', ')})" + ) + end + + def copy_uploads + puts '' + print "copying uploads..." + + source_raw_connection.exec("SELECT * FROM uploads").each do |row| + user_id = row['user_id'].to_i + user_id = user_id_from_imported_id(user_id) if user_id > 0 + absolute_filename = File.join(@uploads_path, row['url'].gsub(/^\/uploads\/[^\/]+\//, '')) + print '.' + + next unless File.exists?(absolute_filename) + + upload = create_upload(user_id, absolute_filename, File.basename(absolute_filename)) + if upload.persisted? + @uploads[row['id']] = upload.id + if @source_cdn + DbHelper.remap(UrlHelper.absolute(row['url'], @source_cdn), upload.url) + end + DbHelper.remap(UrlHelper.absolute(row['url'], @source_base_url), upload.url) + DbHelper.remap(row['url'], upload.url) + else + puts "Error: Upload did not persist for #{absolute_filename}! #{upload.errors.full_messages}" + end + end + puts '' + + copy_model(PostUpload, skip_processing: true) + copy_model(UserAvatar) + end + + def copy_everything_else + [PostTiming, UserArchivedMessage, UnsubscribeKey].each do |k| + copy_model(k, skip_processing: true) + end + + [UserHistory, UserWarning].each do |k| + copy_model(k) + end + + copy_model(Notification, mapping: @notifications) + end + + def copy_badges + copy_model(BadgeGrouping, mapping: @badge_groupings, skip_processing: true) + + puts "merging badges..." + columns = Badge.columns.map(&:name) + imported_ids = [] + last_id = Badge.unscoped.maximum(:id) + + sql = "COPY badges (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + @raw_connection.copy_data(sql, @encoder) do + source_raw_connection.exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM badges").each do |row| + + if existing = Badge.where(name: row['name']).first + @badges[row['id']] = existing.id + next + end + + old_id = row['id'] + row['id'] = (last_id += 1) + @badges[old_id.to_s] = row['id'] + + row['badge_grouping_id'] = @badge_groupings[row['badge_grouping_id']] if row['badge_grouping_id'] + + @raw_connection.put_copy_data(row.values) + end + end + + @sequences[Badge.sequence_name] = last_id + 1 + + copy_model(UserBadge, is_a_user_model: true) + end + + def copy_model(klass, skip_if_merged: false, is_a_user_model: false, skip_processing: false, mapping: nil, select_sql: nil) + + puts "copying #{klass.table_name}..." + + columns = klass.columns.map(&:name) + has_custom_fields = CUSTOM_FIELDS.include?(klass.name.downcase) + imported_ids = [] + last_id = columns.include?('id') ? (klass.unscoped.maximum(:id) || 1) : nil + + sql = "COPY #{klass.table_name} (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + @raw_connection.copy_data(sql, @encoder) do + source_raw_connection.exec(select_sql || "SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM #{klass.table_name}").each do |row| + if row['user_id'] + old_user_id = row['user_id'].to_i + + next if skip_if_merged && @merged_user_ids.include?(old_user_id) + + if is_a_user_model + next if old_user_id < 1 + next if user_id_from_imported_id(old_user_id).nil? + end + + if old_user_id >= 1 + row['user_id'] = user_id_from_imported_id(old_user_id) + if is_a_user_model && row['user_id'].nil? + raise "user_id nil for user id '#{old_user_id}'" + end + next if row['user_id'].nil? # associated record for a deleted user + end + end + + row['group_id'] = group_id_from_imported_id(row['group_id']) if row['group_id'] + row['category_id'] = category_id_from_imported_id(row['category_id']) if row['category_id'] + if row['topic_id'] && klass != Category + row['topic_id'] = topic_id_from_imported_id(row['topic_id']) + next if row['topic_id'].nil? + end + if row['post_id'] + row['post_id'] = post_id_from_imported_id(row['post_id']) + next if row['post_id'].nil? + end + row['tag_id'] = tag_id_from_imported_id(row['tag_id']) if row['tag_id'] + row['tag_group_id'] = tag_group_id_from_imported_id(row['tag_group_id']) if row['tag_group_id'] + row['upload_id'] = upload_id_from_imported_id(row['upload_id']) if row['upload_id'] + row['deleted_by_id'] = user_id_from_imported_id(row['deleted_by_id']) if row['deleted_by_id'] + row['badge_id'] = badge_id_from_imported_id(row['badge_id']) if row['badge_id'] + + old_id = row['id'] + if old_id && last_id + row['id'] = (last_id += 1) + imported_ids << old_id if has_custom_fields + mapping[old_id.to_s] = row['id'] if mapping + end + + if skip_processing + @raw_connection.put_copy_data(row.values) + else + process_method_name = "process_#{klass.name.underscore}" + + processed = respond_to?(process_method_name) ? send(process_method_name, HashWithIndifferentAccess.new(row)) : row + + if processed + @raw_connection.put_copy_data columns.map { |c| processed[c] } + end + end + end + end + + @sequences[klass.sequence_name] = last_id + 1 if last_id + + if has_custom_fields + id_mapping_method_name = "#{klass.name.downcase}_id_from_imported_id".freeze + return unless respond_to?(id_mapping_method_name) + create_custom_fields(klass.name.downcase, "id", imported_ids) do |imported_id| + { + record_id: send(id_mapping_method_name, imported_id), + value: imported_id, + } + end + end + end + + def process_post(post) + post[:last_editor_id] = user_id_from_imported_id(post[:user_id]) + @topic_id_by_post_id[post[:id]] = post[:topic_id] + post + end + + def process_post_reply(post_reply) + post_reply['reply_id'] = post_id_from_imported_id(post_reply['reply_id']) if post_reply['reply_id'] + post_reply + end + + def process_quoted_post(quoted_post) + quoted_post['quoted_post_id'] = post_id_from_imported_id(quoted_post['quoted_post_id']) if quoted_post['quoted_post_id'] + return nil if quoted_post['quoted_post_id'].nil? + quoted_post + end + + def process_topic_link(topic_link) + topic_link['link_topic_id'] = topic_id_from_imported_id(topic_link['link_topic_id']) if topic_link['link_topic_id'] + topic_link['link_post_id'] = post_id_from_imported_id(topic_link['link_post_id']) if topic_link['link_post_id'] + return nil if topic_link['link_topic_id'].nil? || topic_link['link_post_id'].nil? + topic_link + end + + def process_post_action(post_action) + return nil unless post_action['post_id'].present? + post_action['related_post_id'] = post_id_from_imported_id(post_action['related_post_id']) + post_action['deferred_by_id'] = user_id_from_imported_id(post_action['deferred_by_id']) + post_action['agreed_by_id'] = user_id_from_imported_id(post_action['agreed_by_id']) + post_action['disagreed_by_id'] = user_id_from_imported_id(post_action['disagreed_by_id']) + post_action + end + + def process_user_action(user_action) + user_action['target_topic_id'] = topic_id_from_imported_id(user_action['target_topic_id']) if user_action['target_topic_id'] + user_action['target_post_id'] = post_id_from_imported_id(user_action['target_post_id']) if user_action['target_post_id'] + user_action['target_user_id'] = user_id_from_imported_id(user_action['target_user_id']) if user_action['target_user_id'] + user_action['acting_user_id'] = user_id_from_imported_id(user_action['acting_user_id']) if user_action['acting_user_id'] + user_action['queued_post_id'] = post_id_from_imported_id(user_action['queued_post_id']) if user_action['queued_post_id'] + user_action + end + + def process_tag_group(tag_group) + tag_group['parent_tag_id'] = tag_id_from_imported_id(tag_group['parent_tag_id']) if tag_group['parent_tag_id'] + tag_group + end + + def process_category_group(category_group) + return nil if category_group['category_id'].nil? || category_group['group_id'].nil? + category_group + end + + def process_group_user(group_user) + if @auto_group_ids.include?(group_user['group_id'].to_i) && + @merged_user_ids.include?(group_user['user_id'].to_i) + return nil + end + return nil if group_user['user_id'].to_i < 1 + group_user + end + + def process_group_history(group_history) + group_history['acting_user_id'] = user_id_from_imported_id(group_history['acting_user_id']) if group_history['acting_user_id'] + group_history['target_user_id'] = user_id_from_imported_id(group_history['target_user_id']) if group_history['target_user_id'] + group_history + end + + def process_group_archived_message(gam) + return nil unless gam['topic_id'].present? && gam['group_id'].present? + gam + end + + def process_topic_link(topic_link) + topic_link['link_topic_id'] = topic_id_from_imported_id(topic_link['link_topic_id']) if topic_link['link_topic_id'] + topic_link['link_post_id'] = post_id_from_imported_id(topic_link['link_post_id']) if topic_link['link_post_id'] + topic_link + end + + def process_user_avatar(user_avatar) + user_avatar['custom_upload_id'] = upload_id_from_imported_id(user_avatar['custom_upload_id']) if user_avatar['custom_upload_id'] + user_avatar['gravatar_upload_id'] = upload_id_from_imported_id(user_avatar['gravatar_upload_id']) if user_avatar['gravatar_upload_id'] + user_avatar + end + + def process_user_history(user_history) + user_history['acting_user_id'] = user_id_from_imported_id(user_history['acting_user_id']) if user_history['acting_user_id'] + user_history['target_user_id'] = user_id_from_imported_id(user_history['target_user_id']) if user_history['target_user_id'] + user_history + end + + def process_user_warning(user_warning) + user_warning['created_by_id'] = user_id_from_imported_id(user_warning['created_by_id']) if user_warning['created_by_id'] + user_warning + end + + def process_notification(notification) + notification['post_action_id'] = post_action_id_from_imported_id(notification['post_action_id']) if notification['post_action_id'] + notification + end + + def process_facebook_user_info(r) + return nil if FacebookUserInfo.where(facebook_user_id: r['facebook_user_id']).exists? + r + end + + def process_github_user_info(r) + return nil if GithubUserInfo.where(github_user_id: r['github_user_id']).exists? + r + end + + def process_google_user_info(r) + return nil if GoogleUserInfo.where(google_user_id: r['google_user_id']).exists? + r + end + + def process_instagram_user_info(r) + return nil if InstagramUserInfo.where(instagram_user_id: r['instagram_user_id']).exists? + r + end + + def process_oauth2_user_info(r) + return nil if Oauth2UserInfo.where(uid: r['uid'], provider: r['provider']).exists? + r + end + + def process_single_sign_on_record(r) + return nil if SingleSignOnRecord.where(external_id: r['external_id']).exists? + r + end + + def process_twitter_user_info(r) + return nil if TwitterUserInfo.where(twitter_user_id: r['twitter_user_id']).exists? + r + end + + def process_user_badge(user_badge) + user_badge['granted_by_id'] = user_id_from_imported_id(user_badge['granted_by_id']) if user_badge['granted_by_id'] + user_badge['notification_id'] = notification_id_from_imported_id(user_badge['notification_id']) if user_badge['notification_id'] + user_badge + end + + def user_id_from_imported_id(id) + return id if id.to_i < 1 + super(id) + end + + def group_id_from_imported_id(id) + return id if @auto_group_ids.include?(id&.to_i) + super(id) + end + + def tag_id_from_imported_id(id) + @tags[id.to_s] + end + + def tag_group_id_from_imported_id(id) + @tag_groups[id.to_s] + end + + def upload_id_from_imported_id(id) + @uploads[id.to_s] + end + + def post_action_id_from_imported_id(id) + @post_actions[id.to_s] + end + + def badge_id_from_imported_id(id) + @badges[id.to_s] + end + + def notification_id_from_imported_id(id) + @notifications[id.to_s] + end + + def fix_primary_keys + @sequences.each do |sequence_name, val| + sql = "SELECT setval('#{sequence_name}', #{val})" + @raw_connection.exec(sql) + end + end + +end + +BulkImport::DiscourseMerger.new.start diff --git a/script/bulk_import/vanilla.rb b/script/bulk_import/vanilla.rb index 771a3ef67b..584b995619 100644 --- a/script/bulk_import/vanilla.rb +++ b/script/bulk_import/vanilla.rb @@ -55,7 +55,7 @@ class BulkImport::Vanilla < BulkImport::Base # SiteSetting.port = 3000 # SiteSetting.automatic_backups_enabled = false - # SiteSetting.disable_emails = true + # SiteSetting.disable_emails = "non-staff" # etc. import_users diff --git a/script/discourse b/script/discourse index 13894282dd..22929741a4 100755 --- a/script/discourse +++ b/script/discourse @@ -264,6 +264,8 @@ WHERE table_schema='public' and (data_type like 'char%' or data_type like 'text% puts "#{result.cmd_tuples} rows affected!" rescue => ex puts "Error: #{ex}" + puts "The remap has only been partially applied due to the error above. Please re-run the script again." + exit(1) end end end diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index fc50cc13cd..05d66b0d1d 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -75,7 +75,7 @@ class ImportScripts::Base min_personal_message_post_length: 1, min_personal_message_title_length: 1, allow_duplicate_topic_titles: true, - disable_emails: true, + disable_emails: 'yes', max_attachment_size_kb: 102400, max_image_size_kb: 102400, authorized_extensions: '*' @@ -626,7 +626,7 @@ class ImportScripts::Base def update_topic_status puts "", "Updating topic status" - Topic.exec_sql(<<~SQL) + DB.exec(<<~SQL) UPDATE topics AS t SET closed = TRUE WHERE EXISTS( @@ -636,7 +636,7 @@ class ImportScripts::Base ) SQL - Topic.exec_sql(<<~SQL) + DB.exec(<<~SQL) UPDATE topics AS t SET archived = TRUE WHERE EXISTS( @@ -646,7 +646,7 @@ class ImportScripts::Base ) SQL - TopicCustomField.exec_sql(<<~SQL) + DB.exec(<<~SQL) DELETE FROM topic_custom_fields WHERE name IN ('import_closed', 'import_archived') SQL @@ -654,7 +654,7 @@ class ImportScripts::Base def update_bumped_at puts "", "Updating bumped_at on topics" - Post.exec_sql("update topics t set bumped_at = COALESCE((select max(created_at) from posts where topic_id = t.id and post_type = #{Post.types[:regular]}), bumped_at)") + DB.exec("update topics t set bumped_at = COALESCE((select max(created_at) from posts where topic_id = t.id and post_type = #{Post.types[:regular]}), bumped_at)") end def update_last_posted_at @@ -674,7 +674,7 @@ class ImportScripts::Base AND users.last_posted_at <> lpa.last_posted_at SQL - User.exec_sql(sql) + DB.exec(sql) end def update_user_stats @@ -707,7 +707,7 @@ class ImportScripts::Base AND user_stats.first_post_created_at <> sub.first_post_created_at SQL - User.exec_sql(sql) + DB.exec(sql) puts "", "Updating user post_count..." @@ -725,7 +725,7 @@ class ImportScripts::Base AND user_stats.post_count <> sub.post_count SQL - User.exec_sql(sql) + DB.exec(sql) puts "", "Updating user topic_count..." @@ -743,15 +743,15 @@ class ImportScripts::Base AND user_stats.topic_count <> sub.topic_count SQL - User.exec_sql(sql) + DB.exec(sql) end # scripts that are able to import last_seen_at from the source data should override this method def update_last_seen_at puts "", "Updating last seen at on users" - User.exec_sql("UPDATE users SET last_seen_at = created_at WHERE last_seen_at IS NULL") - User.exec_sql("UPDATE users SET last_seen_at = last_posted_at WHERE last_posted_at IS NOT NULL") + DB.exec("UPDATE users SET last_seen_at = created_at WHERE last_seen_at IS NULL") + DB.exec("UPDATE users SET last_seen_at = last_posted_at WHERE last_posted_at IS NOT NULL") end def update_feature_topic_users diff --git a/script/import_scripts/base/uploader.rb b/script/import_scripts/base/uploader.rb index ca7b65bb1d..f9d3e135ff 100644 --- a/script/import_scripts/base/uploader.rb +++ b/script/import_scripts/base/uploader.rb @@ -9,21 +9,34 @@ module ImportScripts # Expects path to be the full path and filename of the source file. # @return [Upload] def create_upload(user_id, path, source_filename) - tmp = Tempfile.new('discourse-upload') - src = File.open(path) - FileUtils.copy_stream(src, tmp) - src.close - tmp.rewind + tmp = copy_to_tempfile(path) UploadCreator.new(tmp, source_filename).create_for(user_id) rescue => e - Rails.logger.error("Failed to create upload: #{e}") + puts "Failed to create upload: #{e}" nil ensure tmp.close rescue nil tmp.unlink rescue nil end + def create_avatar(user, avatar_path) + tempfile = copy_to_tempfile(avatar_path) + filename = "avatar#{File.extname(avatar_path)}" + upload = UploadCreator.new(tempfile, filename, type: "avatar").create_for(user.id) + + if upload.present? && upload.persisted? + user.create_user_avatar + user.user_avatar.update(custom_upload_id: upload.id) + user.update(uploaded_avatar_id: upload.id) + else + puts "Failed to upload avatar for user #{user.username}: #{avatar_path}" + puts upload.errors.inspect if upload + end + ensure + tempfile.close! if tempfile + end + def html_for_upload(upload, display_filename) if FileHelper.is_image?(upload.url) embedded_image_html(upload) @@ -35,11 +48,25 @@ module ImportScripts def embedded_image_html(upload) image_width = [upload.width, SiteSetting.max_image_width].compact.min image_height = [upload.height, SiteSetting.max_image_height].compact.min - %Q[
] + upload_name = upload.short_url || upload.url + %Q~![#{upload.original_filename}|#{image_width}x#{image_height}](#{upload_name})~ end def attachment_html(upload, display_filename) "#{display_filename} (#{number_to_human_size(upload.filesize)})" end + + private + + def copy_to_tempfile(source_path) + tmp = Tempfile.new('discourse-upload') + + File.open(source_path) do |source_stream| + IO.copy_stream(source_stream, tmp) + end + + tmp.rewind + tmp + end end end diff --git a/script/import_scripts/ipboard.rb b/script/import_scripts/ipboard.rb index 18708898bf..f1b10b6a5b 100644 --- a/script/import_scripts/ipboard.rb +++ b/script/import_scripts/ipboard.rb @@ -70,7 +70,7 @@ class ImportScripts::IpboardSQL < ImportScripts::Base # Site settings # ################# # don't send any emails - SiteSetting.disable_emails = true + SiteSetting.disable_emails = "non-staff" # don't send digests (so you can enable email without users noticing) SiteSetting.disable_digest_emails = true # keep site and users private diff --git a/script/import_scripts/ipboard3.rb b/script/import_scripts/ipboard3.rb index 5a9812f85d..53d808aa69 100644 --- a/script/import_scripts/ipboard3.rb +++ b/script/import_scripts/ipboard3.rb @@ -267,7 +267,7 @@ class ImportScripts::IPBoard3 < ImportScripts::Base WHERE id IN (SELECT topic_id FROM closed_topic_ids) SQL - Topic.exec_sql(sql, @closed_topic_ids) + DB.exec(sql, @closed_topic_ids) end def import_personal_topics diff --git a/script/import_scripts/jive_api.rb b/script/import_scripts/jive_api.rb index f8c3734a35..923904aef8 100644 --- a/script/import_scripts/jive_api.rb +++ b/script/import_scripts/jive_api.rb @@ -325,7 +325,7 @@ class ImportScripts::JiveApi < ImportScripts::Base def mark_topics_as_solved puts "", "Marking topics as solved..." - PostAction.exec_sql <<-SQL + DB.exec <<~SQL INSERT INTO topic_custom_fields (name, value, topic_id, created_at, updated_at) SELECT 'accepted_answer_post_id', pcf.post_id, p.topic_id, p.created_at, p.created_at FROM post_custom_fields pcf diff --git a/script/import_scripts/lithium.rb b/script/import_scripts/lithium.rb index 108fec921e..d83fe105e3 100644 --- a/script/import_scripts/lithium.rb +++ b/script/import_scripts/lithium.rb @@ -70,6 +70,7 @@ class ImportScripts::Lithium < ImportScripts::Base import_groups import_categories import_users + import_user_visits import_topics import_posts import_likes @@ -103,6 +104,7 @@ class ImportScripts::Lithium < ImportScripts::Base user_count = mysql_query("SELECT COUNT(*) count FROM users").first["count"] avatar_files = Dir.entries(AVATAR_DIR) + duplicate_emails = mysql_query("SELECT email_lower FROM users GROUP BY email_lower HAVING COUNT(email_lower) > 1").map { |e| [e["email_lower"], 0] }.to_h batches(BATCH_SIZE) do |offset| users = mysql_query <<-SQL @@ -129,17 +131,25 @@ class ImportScripts::Lithium < ImportScripts::Base SQL create_users(users, total: user_count, offset: offset) do |user| - profile = profiles.select { |p| p["user_id"] == user["id"] } + user_id = user["id"] + profile = profiles.select { |p| p["user_id"] == user_id } result = profile.select { |p| p["param"] == "profile.location" } location = result.count > 0 ? result.first["nvalue"] : nil username = user["login_canon"] username = USERNAME_MAPPINGS[username] if USERNAME_MAPPINGS[username].present? + email = user["email"].presence || fake_email + email_lower = email.downcase + if duplicate_emails.key?(email_lower) + duplicate_emails[email_lower] += 1 + email.sub!("@", "+#{duplicate_emails[email_lower]}@") if duplicate_emails[email_lower] > 1 + end + { - id: user["id"], + id: user_id, name: user["nlogin"], username: username, - email: user["email"].presence || fake_email, + email: email, location: location, custom_fields: user_custom_fields(user, profile), # website: user["homepage"].strip, @@ -176,6 +186,35 @@ class ImportScripts::Lithium < ImportScripts::Base end end + def import_user_visits + puts "", "importing user visits" + + batches(BATCH_SIZE) do |offset| + visits = mysql_query <<-SQL + SELECT user_id, login_time + FROM user_log + ORDER BY user_id + LIMIT #{BATCH_SIZE} + OFFSET #{offset} + SQL + + break if visits.size < 1 + + user_ids = visits.uniq { |v| v["user_id"] } + + user_ids.each do |user_id| + user = UserCustomField.find_by(name: "import_id", value: user_id).try(:user) + raise "User not found for id #{user_id}" if user.blank? + + user_visits = visits.select { |v| v["user_id"] == user_id } + user_visits.each do |v| + date = unix_time(v["login_time"]) + user.update_visit_record!(date) + end + end + end + end + def user_custom_fields(user, profile) fields = Hash.new @@ -496,7 +535,7 @@ class ImportScripts::Lithium < ImportScripts::Base end puts "loading data into temp table" - PostAction.exec_sql("create temp table like_data(user_id int, post_id int, created_at timestamp without time zone)") + DB.exec("create temp table like_data(user_id int, post_id int, created_at timestamp without time zone)") PostAction.transaction do results.each do |result| @@ -505,17 +544,17 @@ class ImportScripts::Lithium < ImportScripts::Base next unless result["user_id"] && result["post_id"] - PostAction.exec_sql("INSERT INTO like_data VALUES (:user_id,:post_id,:created_at)", - user_id: result["user_id"], - post_id: result["post_id"], - created_at: result["created_at"] - ) + DB.exec("INSERT INTO like_data VALUES (:user_id,:post_id,:created_at)", + user_id: result["user_id"], + post_id: result["post_id"], + created_at: result["created_at"] + ) end end puts "creating missing post actions" - PostAction.exec_sql <<-SQL + DB.exec <<~SQL INSERT INTO post_actions (post_id, user_id, post_action_type_id, created_at, updated_at) SELECT l.post_id, l.user_id, 2, l.created_at, l.created_at FROM like_data l @@ -524,7 +563,7 @@ class ImportScripts::Lithium < ImportScripts::Base SQL puts "creating missing user actions" - UserAction.exec_sql <<-SQL + DB.exec <<~SQL INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at) SELECT pa.user_id, 1, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at FROM post_actions pa @@ -535,7 +574,7 @@ class ImportScripts::Lithium < ImportScripts::Base SQL # reverse action - UserAction.exec_sql <<-SQL + DB.exec <<~SQL INSERT INTO user_actions (user_id, action_type, target_topic_id, target_post_id, acting_user_id, created_at, updated_at) SELECT p.user_id, 2, p.topic_id, p.id, pa.user_id, pa.created_at, pa.created_at FROM post_actions pa @@ -547,7 +586,7 @@ class ImportScripts::Lithium < ImportScripts::Base SQL puts "updating like counts on posts" - Post.exec_sql <<-SQL + DB.exec <<~SQL UPDATE posts SET like_count = coalesce(cnt,0) FROM ( SELECT post_id, count(*) cnt @@ -561,7 +600,7 @@ class ImportScripts::Lithium < ImportScripts::Base puts "updating like counts on topics" - Post.exec_sql <<-SQL + DB.exec <<-SQL UPDATE topics SET like_count = coalesce(cnt,0) FROM ( SELECT topic_id, sum(like_count) cnt @@ -588,7 +627,7 @@ class ImportScripts::Lithium < ImportScripts::Base end puts "loading data into temp table" - PostAction.exec_sql("create temp table accepted_data(post_id int primary key)") + DB.exec("create temp table accepted_data(post_id int primary key)") PostAction.transaction do results.each do |result| @@ -596,7 +635,7 @@ class ImportScripts::Lithium < ImportScripts::Base next unless result["post_id"] - PostAction.exec_sql("INSERT INTO accepted_data VALUES (:post_id)", + DB.exec("INSERT INTO accepted_data VALUES (:post_id)", post_id: result["post_id"] ) @@ -604,7 +643,7 @@ class ImportScripts::Lithium < ImportScripts::Base end puts "deleting dupe answers" - PostAction.exec_sql <<-SQL + DB.exec <<~SQL DELETE FROM accepted_data WHERE post_id NOT IN ( SELECT post_id FROM ( @@ -617,7 +656,7 @@ class ImportScripts::Lithium < ImportScripts::Base SQL puts "importing accepted answers" - PostAction.exec_sql <<-SQL + DB.exec <<~SQL INSERT into post_custom_fields (name, value, post_id, created_at, updated_at) SELECT 'is_accepted_answer', 'true', a.post_id, current_timestamp, current_timestamp FROM accepted_data a @@ -626,7 +665,7 @@ class ImportScripts::Lithium < ImportScripts::Base SQL puts "marking accepted topics" - PostAction.exec_sql <<-SQL + DB.exec <<~SQL INSERT into topic_custom_fields (name, value, topic_id, created_at, updated_at) SELECT 'accepted_answer_post_id', a.post_id::varchar, p.topic_id, current_timestamp, current_timestamp FROM accepted_data a @@ -758,10 +797,10 @@ class ImportScripts::Lithium < ImportScripts::Base results.map { |r| r["post_id"] }.each_slice(500) do |ids| mapped = ids.map { |id| existing_map[id] }.compact - Topic.exec_sql(" - UPDATE topics SET closed = true - WHERE id IN (SELECT topic_id FROM posts where id in (:ids)) - ", ids: mapped) if mapped.present? + DB.exec(<<~SQL, ids: mapped) if mapped.present? + UPDATE topics SET closed = true + WHERE id IN (SELECT topic_id FROM posts where id in (:ids)) + SQL end end @@ -780,8 +819,8 @@ class ImportScripts::Lithium < ImportScripts::Base WHERE pm.id IS NULL AND f.name = 'import_unique_id' SQL - r = Permalink.exec_sql sql - puts "#{r.cmd_tuples} permalinks to topics added!" + r = DB.exec sql + puts "#{r} permalinks to topics added!" sql = <<-SQL INSERT INTO permalinks (url, post_id, created_at, updated_at) @@ -792,13 +831,14 @@ SQL WHERE pm.id IS NULL AND f.name = 'import_unique_id' SQL - r = Permalink.exec_sql sql - puts "#{r.cmd_tuples} permalinks to posts added!" + r = DB.exec sql + puts "#{r} permalinks to posts added!" end def find_upload(user_id, attachment_id, real_filename) - filename = File.join(ATTACHMENT_DIR, "#{attachment_id.to_s.rjust(4, "0")}.dat") + filename = attachment_id.to_s.rjust(4, "0") + filename = File.join(ATTACHMENT_DIR, "000#{filename[0]}/#{filename}.dat") unless File.exists?(filename) puts "Attachment file doesn't exist: #{filename}" @@ -952,7 +992,7 @@ SQL user = UserCustomField.find_by(name: 'import_id', value: uid).try(:user) if user.present? username = user.username - span = doc.create_element "span" + span = l.document.create_element "span" span.inner_html = "@#{username}" l.replace span end diff --git a/script/import_scripts/mbox.rb b/script/import_scripts/mbox.rb index d0b379aa12..2f125cbf33 100755 --- a/script/import_scripts/mbox.rb +++ b/script/import_scripts/mbox.rb @@ -27,7 +27,7 @@ class ImportScripts::Mbox < ImportScripts::Base BATCH_SIZE = 1000 # Site settings - SiteSetting.disable_emails = true + SiteSetting.disable_emails = "non-staff" # Comment out if each file contains a single message # Use formail to split yourself: http://linuxcommand.org/man_pages/formail1.html diff --git a/script/import_scripts/modx.rb b/script/import_scripts/modx.rb index 191135f6bf..eeaa21be95 100644 --- a/script/import_scripts/modx.rb +++ b/script/import_scripts/modx.rb @@ -24,7 +24,7 @@ class ImportScripts::Modx < ImportScripts::Base def initialize super - SiteSetting.disable_emails = true + SiteSetting.disable_emails = "non-staff" @old_username_to_new_usernames = {} @@ -236,7 +236,7 @@ FROM #{TABLE_PREFIX}discuss_users def not_mark_topics_as_solved puts "", "Marking topics as solved..." - PostAction.exec_sql <<-SQL + DB.exec <<~SQL INSERT INTO topic_custom_fields (name, value, topic_id, created_at, updated_at) SELECT 'accepted_answer_post_id', pcf.post_id, p.topic_id, p.created_at, p.created_at FROM post_custom_fields pcf @@ -469,7 +469,7 @@ FROM #{TABLE_PREFIX}discuss_users WHERE id IN (SELECT topic_id FROM closed_topic_ids) SQL - Topic.exec_sql(sql, closed_topic_ids) + DB.exec(sql, closed_topic_ids) end def not_post_process_posts diff --git a/script/import_scripts/mybb.rb b/script/import_scripts/mybb.rb index f9201ecacf..cc3ed9e662 100644 --- a/script/import_scripts/mybb.rb +++ b/script/import_scripts/mybb.rb @@ -37,7 +37,7 @@ class ImportScripts::MyBB < ImportScripts::Base end def execute - SiteSetting.disable_emails = true + SiteSetting.disable_emails = "non-staff" import_users import_categories import_posts diff --git a/script/import_scripts/mylittleforum.rb b/script/import_scripts/mylittleforum.rb index f0ec152472..4b10494eb4 100644 --- a/script/import_scripts/mylittleforum.rb +++ b/script/import_scripts/mylittleforum.rb @@ -33,7 +33,7 @@ class ImportScripts::MylittleforumSQL < ImportScripts::Base QUIET = true # Site settings - SiteSetting.disable_emails = true + SiteSetting.disable_emails = "non-staff" if FORCE_HOSTNAME SiteSetting.force_hostname = FORCE_HOSTNAME end diff --git a/script/import_scripts/phpbb3/importers/avatar_importer.rb b/script/import_scripts/phpbb3/importers/avatar_importer.rb index a9cd22d4c9..2178ed800c 100644 --- a/script/import_scripts/phpbb3/importers/avatar_importer.rb +++ b/script/import_scripts/phpbb3/importers/avatar_importer.rb @@ -47,16 +47,16 @@ module ImportScripts::PhpBB3 def get_avatar_path(avatar_type, filename) case avatar_type - when Constants::AVATAR_TYPE_UPLOADED then + when Constants::AVATAR_TYPE_UPLOADED, Constants::AVATAR_TYPE_STRING_UPLOADED then filename.gsub!(/_[0-9]+\./, '.') # we need 1337.jpg, not 1337_2983745.jpg get_uploaded_path(filename) - when Constants::AVATAR_TYPE_GALLERY then + when Constants::AVATAR_TYPE_GALLERY, Constants::AVATAR_TYPE_STRING_GALLERY then get_gallery_path(filename) - when Constants::AVATAR_TYPE_REMOTE then + when Constants::AVATAR_TYPE_REMOTE, Constants::AVATAR_TYPE_STRING_REMOTE then download_avatar(filename) - else - Rails.logger.error("Invalid avatar type #{avatar_type}. Skipping...") - nil + else + puts "Invalid avatar type #{avatar_type}. Skipping..." + nil end end @@ -97,13 +97,13 @@ module ImportScripts::PhpBB3 def is_allowed_avatar_type?(avatar_type) case avatar_type - when Constants::AVATAR_TYPE_UPLOADED then + when Constants::AVATAR_TYPE_UPLOADED, Constants::AVATAR_TYPE_STRING_UPLOADED then @settings.import_uploaded_avatars - when Constants::AVATAR_TYPE_REMOTE then + when Constants::AVATAR_TYPE_REMOTE, Constants::AVATAR_TYPE_STRING_REMOTE then @settings.import_remote_avatars - when Constants::AVATAR_TYPE_GALLERY then + when Constants::AVATAR_TYPE_GALLERY, Constants::AVATAR_TYPE_STRING_GALLERY then @settings.import_gallery_avatars - else + else false end end diff --git a/script/import_scripts/phpbb3/support/constants.rb b/script/import_scripts/phpbb3/support/constants.rb index af7482d5da..6e9612e678 100644 --- a/script/import_scripts/phpbb3/support/constants.rb +++ b/script/import_scripts/phpbb3/support/constants.rb @@ -19,6 +19,10 @@ module ImportScripts::PhpBB3 AVATAR_TYPE_REMOTE = 2 AVATAR_TYPE_GALLERY = 3 + AVATAR_TYPE_STRING_UPLOADED = 'avatar.driver.upload' + AVATAR_TYPE_STRING_REMOTE = 'avatar.driver.remote' + AVATAR_TYPE_STRING_GALLERY = 'avatar.driver.local' + FORUM_TYPE_CATEGORY = 0 FORUM_TYPE_POST = 1 FORUM_TYPE_LINK = 2 diff --git a/script/import_scripts/quandora/quandora_question.rb b/script/import_scripts/quandora/quandora_question.rb index 6105ef895d..9eb59c9e30 100644 --- a/script/import_scripts/quandora/quandora_question.rb +++ b/script/import_scripts/quandora/quandora_question.rb @@ -8,102 +8,102 @@ class QuandoraQuestion @question = JSON.parse question_json end - def topic - topic = {} - topic[:id] = @question['uid'] - topic[:author_id] = @question['author']['uid'] - topic[:title] = unescape @question['title'] - topic[:raw] = unescape @question['content'] - topic[:created_at] = Time.parse @question['created'] - topic - end + def topic + topic = {} + topic[:id] = @question['uid'] + topic[:author_id] = @question['author']['uid'] + topic[:title] = unescape @question['title'] + topic[:raw] = unescape @question['content'] + topic[:created_at] = Time.parse @question['created'] + topic + end - def users - users = {} - user = user_from_author @question['author'] - users[user[:id]] = user - replies.each do |reply| - user = user_from_author reply[:author] - users[user[:id]] = user - end - users.values.to_a - end + def users + users = {} + user = user_from_author @question['author'] + users[user[:id]] = user + replies.each do |reply| + user = user_from_author reply[:author] + users[user[:id]] = user + end + users.values.to_a + end - def user_from_author(author) - email = author['email'] - email = "#{author['uid']}@noemail.com" unless email + def user_from_author(author) + email = author['email'] + email = "#{author['uid']}@noemail.com" unless email - user = {} - user[:id] = author['uid'] - user[:name] = "#{author['firstName']} #{author['lastName']}" - user[:email] = email - user[:staged] = true - user - end + user = {} + user[:id] = author['uid'] + user[:name] = "#{author['firstName']} #{author['lastName']}" + user[:email] = email + user[:staged] = true + user + end - def replies - posts = [] - answers = @question['answersList'] - comments = @question['comments'] - comments.each_with_index do |comment, i| - posts << post_from_comment(comment, i, @question) - end - answers.each do |answer| - posts << post_from_answer(answer) - comments = answer['comments'] - comments.each_with_index do |comment, i| - posts << post_from_comment(comment, i, answer) - end - end - order_replies posts - end + def replies + posts = [] + answers = @question['answersList'] + comments = @question['comments'] + comments.each_with_index do |comment, i| + posts << post_from_comment(comment, i, @question) + end + answers.each do |answer| + posts << post_from_answer(answer) + comments = answer['comments'] + comments.each_with_index do |comment, i| + posts << post_from_comment(comment, i, answer) + end + end + order_replies posts + end - def order_replies(posts) - posts = posts.sort_by { |p| p[:created_at] } - posts.each_with_index do |p, i| - p[:post_number] = i + 2 - end - posts.each do |p| - parent = posts.select { |pp| pp[:id] == p[:parent_id] } - p[:reply_to_post_number] = parent[0][:post_number] if parent.size > 0 - end - posts - end + def order_replies(posts) + posts = posts.sort_by { |p| p[:created_at] } + posts.each_with_index do |p, i| + p[:post_number] = i + 2 + end + posts.each do |p| + parent = posts.select { |pp| pp[:id] == p[:parent_id] } + p[:reply_to_post_number] = parent[0][:post_number] if parent.size > 0 + end + posts + end - def post_from_answer(answer) - post = {} - post[:id] = answer['uid'] - post[:parent_id] = @question['uid'] - post[:author] = answer['author'] - post[:author_id] = answer['author']['uid'] - post[:raw] = unescape answer['content'] - post[:created_at] = Time.parse answer['created'] - post - end + def post_from_answer(answer) + post = {} + post[:id] = answer['uid'] + post[:parent_id] = @question['uid'] + post[:author] = answer['author'] + post[:author_id] = answer['author']['uid'] + post[:raw] = unescape answer['content'] + post[:created_at] = Time.parse answer['created'] + post + end - def post_from_comment(comment, index, parent) - if comment['created'] - created_at = Time.parse comment['created'] - else - created_at = Time.parse parent['created'] - end - parent_id = parent['uid'] - parent_id = "#{parent['uid']}-#{index - 1}" if index > 0 - post = {} - id = "#{parent['uid']}-#{index}" - post[:id] = id - post[:parent_id] = parent_id - post[:author] = comment['author'] - post[:author_id] = comment['author']['uid'] - post[:raw] = unescape comment['text'] - post[:created_at] = created_at - post - end + def post_from_comment(comment, index, parent) + if comment['created'] + created_at = Time.parse comment['created'] + else + created_at = Time.parse parent['created'] + end + parent_id = parent['uid'] + parent_id = "#{parent['uid']}-#{index - 1}" if index > 0 + post = {} + id = "#{parent['uid']}-#{index}" + post[:id] = id + post[:parent_id] = parent_id + post[:author] = comment['author'] + post[:author_id] = comment['author']['uid'] + post[:raw] = unescape comment['text'] + post[:created_at] = created_at + post + end private - def unescape(html) - return nil unless html - CGI.unescapeHTML html - end + def unescape(html) + return nil unless html + CGI.unescapeHTML html + end end diff --git a/script/import_scripts/socialcast/socialcast_message.rb b/script/import_scripts/socialcast/socialcast_message.rb index a82149f2ab..e121c5695d 100644 --- a/script/import_scripts/socialcast/socialcast_message.rb +++ b/script/import_scripts/socialcast/socialcast_message.rb @@ -18,81 +18,81 @@ class SocialcastMessage } } - def initialize(message_json) - @parsed_json = JSON.parse message_json - end + def initialize(message_json) + @parsed_json = JSON.parse message_json + end - def topic - topic = {} - topic[:id] = @parsed_json['id'] - topic[:author_id] = @parsed_json['user']['id'] - topic[:title] = title - topic[:raw] = @parsed_json['body'] - topic[:created_at] = Time.parse @parsed_json['created_at'] - topic[:tags] = tags - topic[:category] = category - topic - end + def topic + topic = {} + topic[:id] = @parsed_json['id'] + topic[:author_id] = @parsed_json['user']['id'] + topic[:title] = title + topic[:raw] = @parsed_json['body'] + topic[:created_at] = Time.parse @parsed_json['created_at'] + topic[:tags] = tags + topic[:category] = category + topic + end - def title - CreateTitle.from_body @parsed_json['body'] - end + def title + CreateTitle.from_body @parsed_json['body'] + end - def tags - tags = [] - if group - if TAGS_AND_CATEGORIES[group] - tags = TAGS_AND_CATEGORIES[group][:tags] - else - tags << group - end - end - tags << DEFAULT_TAG - tags - end + def tags + tags = [] + if group + if TAGS_AND_CATEGORIES[group] + tags = TAGS_AND_CATEGORIES[group][:tags] + else + tags << group + end + end + tags << DEFAULT_TAG + tags + end - def category - category = DEFAULT_CATEGORY - if group && TAGS_AND_CATEGORIES[group] - category = TAGS_AND_CATEGORIES[group][:category] - end - category - end + def category + category = DEFAULT_CATEGORY + if group && TAGS_AND_CATEGORIES[group] + category = TAGS_AND_CATEGORIES[group][:category] + end + category + end - def group - @parsed_json['group']['groupname'].downcase if @parsed_json['group'] && @parsed_json['group']['groupname'] - end + def group + @parsed_json['group']['groupname'].downcase if @parsed_json['group'] && @parsed_json['group']['groupname'] + end - def url - @parsed_json['url'] - end + def url + @parsed_json['url'] + end - def message_type - @parsed_json['message_type'] - end + def message_type + @parsed_json['message_type'] + end - def replies - posts = [] - comments = @parsed_json['comments'] - comments.each do |comment| - posts << post_from_comment(comment) - end - posts - end + def replies + posts = [] + comments = @parsed_json['comments'] + comments.each do |comment| + posts << post_from_comment(comment) + end + posts + end - def post_from_comment(comment) - post = {} - post[:id] = comment['id'] - post[:author_id] = comment['user']['id'] - post[:raw] = comment['text'] - post[:created_at] = Time.parse comment['created_at'] - post - end + def post_from_comment(comment) + post = {} + post[:id] = comment['id'] + post[:author_id] = comment['user']['id'] + post[:raw] = comment['text'] + post[:created_at] = Time.parse comment['created_at'] + post + end - private + private - def unescape(html) - return nil unless html - CGI.unescapeHTML html - end + def unescape(html) + return nil unless html + CGI.unescapeHTML html + end end diff --git a/script/import_scripts/socialcast/socialcast_user.rb b/script/import_scripts/socialcast/socialcast_user.rb index 54107b4636..fb4217318c 100644 --- a/script/import_scripts/socialcast/socialcast_user.rb +++ b/script/import_scripts/socialcast/socialcast_user.rb @@ -8,17 +8,17 @@ class SocialcastUser @parsed_json = JSON.parse user_json end - def user - email = @parsed_json['contact_info']['email'] - email = "#{@parsed_json['id']}@noemail.com" unless email + def user + email = @parsed_json['contact_info']['email'] + email = "#{@parsed_json['id']}@noemail.com" unless email - user = {} - user[:id] = @parsed_json['id'] - user[:name] = @parsed_json['name'] - user[:username] = @parsed_json['username'] - user[:email] = email - user[:staged] = true - user - end + user = {} + user[:id] = @parsed_json['id'] + user[:name] = @parsed_json['name'] + user[:username] = @parsed_json['username'] + user[:email] = email + user[:staged] = true + user + end end diff --git a/script/import_scripts/stack_overflow.rb b/script/import_scripts/stack_overflow.rb new file mode 100644 index 0000000000..06754fa0b2 --- /dev/null +++ b/script/import_scripts/stack_overflow.rb @@ -0,0 +1,255 @@ +# cf. https://github.com/rails-sqlserver/tiny_tds#install +require "tiny_tds" +require File.expand_path(File.dirname(__FILE__) + "/base.rb") + +class ImportScripts::StackOverflow < ImportScripts::Base + + BATCH_SIZE ||= 1000 + + def initialize + super + + @client = TinyTds::Client.new( + host: ENV["DB_HOST"], + username: ENV["DB_USERNAME"], + password: ENV["DB_PASSWORD"], + database: ENV["DB_NAME"], + ) + end + + def execute + SiteSetting.tagging_enabled = true + + # TODO: import_groups + import_users + import_posts + import_likes + mark_topics_as_solved + end + + def import_users + puts "", "Importing users..." + + last_user_id = -1 + total = query("SELECT COUNT(*) count FROM Users WHERE Id > 0").first["count"] + + batches(BATCH_SIZE) do |offset| + users = query(<<~SQL + SELECT TOP #{BATCH_SIZE} + Id + , UserTypeId + , CreationDate + , LastLoginDate + , LastLoginIP + , Email + , DisplayName + , WebsiteUrl + , RealName + , Location + , Birthday + , ProfileImageUrl + FROM Users + WHERE Id > 0 + AND Id > #{last_user_id} + ORDER BY Id + SQL + ).to_a + + break if users.empty? + + last_user_id = users[-1]["Id"] + user_ids = users.map { |u| u["Id"] } + + next if all_records_exist?(:users, user_ids) + + create_users(users, total: total, offset: offset) do |u| + { + id: u["Id"], + admin: u["UserTypeId"] == 4, + created_at: u["CreationDate"], + last_seen_at: u["LastLoginDate"], + ip_address: u["LastLoginIP"], + email: u["Email"], + username: u["DisplayName"], + website: u["WebsiteUrl"], + name: u["RealName"], + location: u["Location"], + date_of_birth: u["Birthday"], + post_create_action: proc do |user| + if u["ProfileImageUrl"].present? + UserAvatar.import_url_for_user(u["ProfileImageUrl"], user) rescue nil + end + end + } + end + end + end + + def import_posts + puts "", "Importing posts..." + + last_post_id = -1 + total = query("SELECT COUNT(*) count FROM Posts WHERE PostTypeId IN (1,2,3)").first["count"] + + query("SELECT COUNT(*) count FROM PostComments WHERE PostId IN (SELECT Id FROM Posts WHERE PostTypeId IN (1,2,3))").first["count"] + + batches(BATCH_SIZE) do |offset| + posts = query(<<~SQL + SELECT TOP #{BATCH_SIZE} + Id + , PostTypeId + , CreationDate + , Body + , OwnerUserId AS UserId + , Title + , Tags + , DeletionDate + , ParentId + , IsAcceptedAnswer + , CASE WHEN (ClosedDate IS NOT NULL OR LockedDate IS NOT NULL) THEN 1 ELSE 0 END AS Closed + FROM Posts + WHERE PostTypeId IN (1,2,3) + AND Id > #{last_post_id} + ORDER BY Id + SQL + ).to_a + + break if posts.empty? + + last_post_id = posts[-1]["Id"] + post_ids = posts.map { |p| p["Id"] } + + comments = query(<<~SQL + SELECT CONCAT('Comment-', Id) AS Id + , PostId AS ParentId + , Text + , CreationDate + , UserId + FROM PostComments + WHERE PostId IN (#{post_ids.join(",")}) + ORDER BY Id + SQL + ).to_a + + posts_and_comments = (posts + comments).sort_by { |p| p["CreationDate"] } + post_and_comment_ids = posts_and_comments.map { |p| p["Id"] } + + next if all_records_exist?(:posts, post_and_comment_ids) + + create_posts(posts_and_comments, total: total, offset: offset) do |p| + raw = p["Body"].present? ? HtmlToMarkdown.new(p["Body"]).to_markdown : p["Text"] + + post = { + id: p["Id"], + created_at: p["CreationDate"], + raw: raw, + user_id: user_id_from_imported_user_id(p["UserId"]) || -1, + } + + if p["Title"].present? + post[:wiki] = p["PostTypeId"] = 3 + post[:title] = p["Title"] + post[:tags] = p["Tags"].split("|") + post[:deleted_at] = p["DeletionDate"] + post[:closed] = p["Closed"] == 1 + elsif t = topic_lookup_from_imported_post_id(p["ParentId"]) + post[:custom_fields] = { is_accepted_answer: true } if p["IsAcceptedAnswer"] + post[:topic_id] = t[:topic_id] + post[:reply_to_post_number] = t[:post_number] + else + puts "", "", "#{p["Id"]} was not imported", "", "" + next + end + + post + end + end + end + + LIKE ||= PostActionType.types[:like] + + def import_likes + puts "", "Importing post likes..." + + last_like_id = -1 + + batches(BATCH_SIZE) do |offset| + likes = query(<<~SQL + SELECT TOP #{BATCH_SIZE} + Id + , PostId + , UserId + , CreationDate + FROM Posts2Votes + WHERE VoteTypeId = 2 + AND DeletionDate IS NULL + AND Id > #{last_like_id} + ORDER BY Id + SQL + ).to_a + + break if likes.empty? + + last_like_id = likes[-1]["Id"] + + likes.each do |l| + next unless user_id = user_id_from_imported_user_id(l["UserId"]) + next unless post_id = post_id_from_imported_post_id(l["PostId"]) + next unless user = User.find_by(id: user_id) + next unless post = Post.find_by(id: post_id) + PostAction.act(user, post, LIKE) rescue nil + end + end + + puts "", "Importing comment likes..." + + last_like_id = -1 + total = query("SELECT COUNT(*) count FROM Comments2Votes WHERE VoteTypeId = 2 AND DeletionDate IS NULL").first["count"] + + batches(BATCH_SIZE) do |offset| + likes = query(<<~SQL + SELECT TOP #{BATCH_SIZE} + Id + , CONCAT('Comment-', PostCommentId) AS PostCommentId + , UserId + , CreationDate + FROM Comments2Votes + WHERE VoteTypeId = 2 + AND DeletionDate IS NULL + AND Id > #{last_like_id} + ORDER BY Id + SQL + ).to_a + + break if likes.empty? + + last_like_id = likes[-1]["Id"] + + likes.each do |l| + next unless user_id = user_id_from_imported_user_id(l["UserId"]) + next unless post_id = post_id_from_imported_post_id(l["PostCommentId"]) + next unless user = User.find_by(id: user_id) + next unless post = Post.find_by(id: post_id) + PostAction.act(user, post, LIKE) rescue nil + end + end + end + + def mark_topics_as_solved + puts "", "Marking topics as solved..." + + DB.exec <<~SQL + INSERT INTO topic_custom_fields (name, value, topic_id, created_at, updated_at) + SELECT 'accepted_answer_post_id', pcf.post_id, p.topic_id, p.created_at, p.created_at + FROM post_custom_fields pcf + JOIN posts p ON p.id = pcf.post_id + WHERE pcf.name = 'is_accepted_answer' + SQL + end + + def query(sql) + @client.execute(sql) + end + +end + +ImportScripts::StackOverflow.new.perform diff --git a/script/import_scripts/vanilla.rb b/script/import_scripts/vanilla.rb index 9471290559..0a9b057eec 100644 --- a/script/import_scripts/vanilla.rb +++ b/script/import_scripts/vanilla.rb @@ -28,226 +28,226 @@ class ImportScripts::Vanilla < ImportScripts::Base private - def check_file_exist - raise ArgumentError.new("File does not exist: #{@vanilla_file}") unless File.exist?(@vanilla_file) - end + def check_file_exist + raise ArgumentError.new("File does not exist: #{@vanilla_file}") unless File.exist?(@vanilla_file) + end - def parse_file - puts "parsing file..." - file = read_file + def parse_file + puts "parsing file..." + file = read_file - # TODO: parse header & validate version number - header = file.readline + # TODO: parse header & validate version number + header = file.readline - until file.eof? - line = file.readline - next if line.blank? - next if line.start_with?("//") + until file.eof? + line = file.readline + next if line.blank? + next if line.start_with?("//") - if m = /^Table: (\w+)/.match(line) - # extract table name - table = m[1].underscore.pluralize - # read the data until an empty line - data = [] - # first line is the table definition, turn that into a proper csv header - data << file.readline.split(",").map { |c| c.split(":")[0].underscore }.join(",") - until (line = file.readline).blank? - data << line.strip - end - # PERF: don't parse useless tables - useless_tables = ["user_meta"] - useless_tables << "activities" unless @use_lastest_activity_as_user_bio - next if useless_tables.include?(table) - # parse the data - puts "parsing #{table}..." - parsed_data = CSV.parse(data.join("\n"), headers: true, header_converters: :symbol).map { |row| row.to_hash } - instance_variable_set("@#{table}".to_sym, parsed_data) + if m = /^Table: (\w+)/.match(line) + # extract table name + table = m[1].underscore.pluralize + # read the data until an empty line + data = [] + # first line is the table definition, turn that into a proper csv header + data << file.readline.split(",").map { |c| c.split(":")[0].underscore }.join(",") + until (line = file.readline).blank? + data << line.strip end + # PERF: don't parse useless tables + useless_tables = ["user_meta"] + useless_tables << "activities" unless @use_lastest_activity_as_user_bio + next if useless_tables.include?(table) + # parse the data + puts "parsing #{table}..." + parsed_data = CSV.parse(data.join("\n"), headers: true, header_converters: :symbol).map { |row| row.to_hash } + instance_variable_set("@#{table}".to_sym, parsed_data) end end + end - def read_file - puts "reading file..." - string = File.read(@vanilla_file).gsub("\\N", "") - .gsub(/\\$\n/m, "\\n") - .gsub("\\,", ",") - .gsub(/(? 0 - puts "", "importing first-level categories..." - create_categories(first_level_categories) { |category| import_category(category) } - - # adds other categories - second_level_categories = @categories.select { |c| c[:parent_category_id] != "-1" } - if second_level_categories.count > 0 - puts "", "importing second-level categories..." - create_categories(second_level_categories) { |category| import_category(category) } - end - end - end - - def import_category(category) - c = { - id: category[:category_id], - name: category[:name], - user_id: user_id_from_imported_user_id(category[:insert_user_id]) || Discourse::SYSTEM_USER_ID, - position: category[:sort].to_i, - created_at: parse_category_date(category[:date_inserted]), - description: clean_up(category[:description]), + u = { + id: user[:user_id], + email: user[:email], + username: user[:name], + created_at: parse_date(user[:date_inserted]), + bio_raw: clean_up(bio_raw), + avatar_url: user[:photo], + moderator: @user_roles.select { |ur| ur[:user_id] == user[:user_id] }.map { |ur| ur[:role_id] }.include?(moderator_role_id), + admin: @user_roles.select { |ur| ur[:user_id] == user[:user_id] }.map { |ur| ur[:role_id] }.include?(admin_role_id), } - if category[:parent_category_id] != "-1" - c[:parent_category_id] = category_id_from_imported_category_id(category[:parent_category_id]) - end - c + + u end + end - def parse_category_date(date) - date == "0000-00-00 00:00:00" ? @root_category_created_at : parse_date(date) - end + def import_categories + puts "", "importing categories..." - def import_topics - puts "", "importing topics..." + # save some information about the root category + @root_category = @categories.select { |c| c[:category_id] == "-1" }.first + @root_category_created_at = parse_date(@root_category[:date_inserted]) - create_posts(@discussions) do |discussion| - { - id: "discussion#" + discussion[:discussion_id], - user_id: user_id_from_imported_user_id(discussion[:insert_user_id]) || Discourse::SYSTEM_USER_ID, - title: discussion[:name], - category: category_id_from_imported_category_id(discussion[:category_id]), - raw: clean_up(discussion[:body]), - created_at: parse_date(discussion[:date_inserted]), - } + # removes root category + @categories.reject! { |c| c[:category_id] == "-1" } + + # adds root's child categories + first_level_categories = @categories.select { |c| c[:parent_category_id] == "-1" } + if first_level_categories.count > 0 + puts "", "importing first-level categories..." + create_categories(first_level_categories) { |category| import_category(category) } + + # adds other categories + second_level_categories = @categories.select { |c| c[:parent_category_id] != "-1" } + if second_level_categories.count > 0 + puts "", "importing second-level categories..." + create_categories(second_level_categories) { |category| import_category(category) } end end + end - def import_posts - puts "", "importing posts..." - - create_posts(@comments) do |comment| - next unless t = topic_lookup_from_imported_post_id("discussion#" + comment[:discussion_id]) - - { - id: "comment#" + comment[:comment_id], - user_id: user_id_from_imported_user_id(comment[:insert_user_id]) || Discourse::SYSTEM_USER_ID, - topic_id: t[:topic_id], - raw: clean_up(comment[:body]), - created_at: parse_date(comment[:date_inserted]), - } - end + def import_category(category) + c = { + id: category[:category_id], + name: category[:name], + user_id: user_id_from_imported_user_id(category[:insert_user_id]) || Discourse::SYSTEM_USER_ID, + position: category[:sort].to_i, + created_at: parse_category_date(category[:date_inserted]), + description: clean_up(category[:description]), + } + if category[:parent_category_id] != "-1" + c[:parent_category_id] = category_id_from_imported_category_id(category[:parent_category_id]) end + c + end - def import_private_topics - puts "", "importing private topics..." + def parse_category_date(date) + date == "0000-00-00 00:00:00" ? @root_category_created_at : parse_date(date) + end - create_posts(@conversations) do |conversation| - next if conversation[:first_message_id].blank? + def import_topics + puts "", "importing topics..." - # list all other user ids in the conversation - user_ids_in_conversation = @user_conversations.select { |uc| uc[:conversation_id] == conversation[:conversation_id] && uc[:user_id] != conversation[:insert_user_id] } - .map { |uc| uc[:user_id] } - # retrieve their emails - user_emails_in_conversation = @users.select { |u| user_ids_in_conversation.include?(u[:user_id]) } - .map { |u| u[:email] } - # retrieve their usernames from the database - target_usernames = User.where("email IN (?)", user_emails_in_conversation).pluck(:username).to_a - - next if target_usernames.blank? - - user = find_user_by_import_id(conversation[:insert_user_id]) || Discourse.system_user - first_message = @conversation_messages.select { |cm| cm[:message_id] == conversation[:first_message_id] }.first - - { - archetype: Archetype.private_message, - id: "conversation#" + conversation[:conversation_id], - user_id: user.id, - title: "Private message from #{user.username}", - target_usernames: target_usernames, - raw: clean_up(first_message[:body]), - created_at: parse_date(conversation[:date_inserted]), - } - end + create_posts(@discussions) do |discussion| + { + id: "discussion#" + discussion[:discussion_id], + user_id: user_id_from_imported_user_id(discussion[:insert_user_id]) || Discourse::SYSTEM_USER_ID, + title: discussion[:name], + category: category_id_from_imported_category_id(discussion[:category_id]), + raw: clean_up(discussion[:body]), + created_at: parse_date(discussion[:date_inserted]), + } end + end - def import_private_posts - puts "", "importing private posts..." + def import_posts + puts "", "importing posts..." - first_message_ids = Set.new(@conversations.map { |c| c[:first_message_id] }.to_a) - @conversation_messages.reject! { |cm| first_message_ids.include?(cm[:message_id]) } + create_posts(@comments) do |comment| + next unless t = topic_lookup_from_imported_post_id("discussion#" + comment[:discussion_id]) - create_posts(@conversation_messages) do |message| - next unless t = topic_lookup_from_imported_post_id("conversation#" + message[:conversation_id]) - - { - archetype: Archetype.private_message, - id: "message#" + message[:message_id], - user_id: user_id_from_imported_user_id(message[:insert_user_id]) || Discourse::SYSTEM_USER_ID, - topic_id: t[:topic_id], - raw: clean_up(message[:body]), - created_at: parse_date(message[:date_inserted]), - } - end + { + id: "comment#" + comment[:comment_id], + user_id: user_id_from_imported_user_id(comment[:insert_user_id]) || Discourse::SYSTEM_USER_ID, + topic_id: t[:topic_id], + raw: clean_up(comment[:body]), + created_at: parse_date(comment[:date_inserted]), + } end + end - def parse_date(date) - DateTime.strptime(date, "%Y-%m-%d %H:%M:%S") - end + def import_private_topics + puts "", "importing private topics..." - def clean_up(raw) - return "" if raw.blank? - raw.gsub("\\n", "\n") - .gsub(/<\/?pre\s*>/i, "\n```\n") - .gsub(/<\/?code\s*>/i, "`") - .gsub("<", "<") - .gsub(">", ">") + create_posts(@conversations) do |conversation| + next if conversation[:first_message_id].blank? + + # list all other user ids in the conversation + user_ids_in_conversation = @user_conversations.select { |uc| uc[:conversation_id] == conversation[:conversation_id] && uc[:user_id] != conversation[:insert_user_id] } + .map { |uc| uc[:user_id] } + # retrieve their emails + user_emails_in_conversation = @users.select { |u| user_ids_in_conversation.include?(u[:user_id]) } + .map { |u| u[:email] } + # retrieve their usernames from the database + target_usernames = User.where("email IN (?)", user_emails_in_conversation).pluck(:username).to_a + + next if target_usernames.blank? + + user = find_user_by_import_id(conversation[:insert_user_id]) || Discourse.system_user + first_message = @conversation_messages.select { |cm| cm[:message_id] == conversation[:first_message_id] }.first + + { + archetype: Archetype.private_message, + id: "conversation#" + conversation[:conversation_id], + user_id: user.id, + title: "Private message from #{user.username}", + target_usernames: target_usernames, + raw: clean_up(first_message[:body]), + created_at: parse_date(conversation[:date_inserted]), + } end + end + + def import_private_posts + puts "", "importing private posts..." + + first_message_ids = Set.new(@conversations.map { |c| c[:first_message_id] }.to_a) + @conversation_messages.reject! { |cm| first_message_ids.include?(cm[:message_id]) } + + create_posts(@conversation_messages) do |message| + next unless t = topic_lookup_from_imported_post_id("conversation#" + message[:conversation_id]) + + { + archetype: Archetype.private_message, + id: "message#" + message[:message_id], + user_id: user_id_from_imported_user_id(message[:insert_user_id]) || Discourse::SYSTEM_USER_ID, + topic_id: t[:topic_id], + raw: clean_up(message[:body]), + created_at: parse_date(message[:date_inserted]), + } + end + end + + def parse_date(date) + DateTime.strptime(date, "%Y-%m-%d %H:%M:%S") + end + + def clean_up(raw) + return "" if raw.blank? + raw.gsub("\\n", "\n") + .gsub(/<\/?pre\s*>/i, "\n```\n") + .gsub(/<\/?code\s*>/i, "`") + .gsub("<", "<") + .gsub(">", ">") + end end diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb index 048fea099b..23923d0baa 100644 --- a/script/import_scripts/vbulletin.rb +++ b/script/import_scripts/vbulletin.rb @@ -32,9 +32,11 @@ class ImportScripts::VBulletin < ImportScripts::Base puts "#{DB_USER}:#{DB_PW}@#{DB_HOST} wants #{DB_NAME}" def initialize + @bbcode_to_md = true + super - @old_username_to_new_usernames = {} + @usernames = {} @tz = TZInfo::Timezone.get(TIMEZONE) @@ -107,6 +109,14 @@ EOM end end + def get_username_for_old_username(old_username) + if @usernames.has_key?(old_username) + @usernames[old_username] + else + old_username + end + end + def import_users puts "", "importing users" @@ -116,7 +126,8 @@ EOM batches(BATCH_SIZE) do |offset| users = mysql_query(<<-SQL - SELECT userid, username, homepage, usertitle, usergroupid, joindate, email + SELECT userid, username, homepage, usertitle, usergroupid, joindate, email, + CONCAT(password, ':', salt) AS crypted_password FROM #{TABLE_PREFIX}user WHERE userid > #{last_user_id} ORDER BY userid @@ -140,6 +151,7 @@ EOM id: user["userid"], name: username, username: username, + password: user["crypted_password"], email: email, website: user["homepage"].strip, title: @htmlentities.decode(user["usertitle"]).strip, @@ -147,13 +159,15 @@ EOM created_at: parse_timestamp(user["joindate"]), last_seen_at: parse_timestamp(user["lastvisit"]), post_create_action: proc do |u| - @old_username_to_new_usernames[user["username"]] = u.username import_profile_picture(user, u) import_profile_background(user, u) end } end end + + @usernames = UserCustomField.joins(:user).where(name: 'import_username').pluck('user_custom_fields.value', 'users.username').to_h + end def create_groups_membership @@ -168,10 +182,8 @@ EOM next if user_ids_in_group.size == 0 values = user_ids_in_group.map { |user_id| "(#{group.id}, #{user_id}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" }.join(",") - User.exec_sql <<-SQL - BEGIN; - INSERT INTO group_users (group_id, user_id, created_at, updated_at) VALUES #{values}; - COMMIT; + DB.exec <<~SQL + INSERT INTO group_users (group_id, user_id, created_at, updated_at) VALUES #{values} SQL Group.reset_counters(group.id, :group_users) @@ -321,16 +333,17 @@ EOM t end - # uncomment below lines to create permalink - # topics.each do |thread| - # topic_id = "thread-#{thread["threadid"]}" - # topic = topic_lookup_from_imported_post_id(topic_id) - # if topic.present? - # title_slugified = thread["title"].gsub(" ","-").gsub(".","-") if thread["title"].present? - # url_slug = "threads/#{thread["threadid"]}-#{title_slugified}" if thread["title"].present? - # Permalink.create(url: url_slug, topic_id: topic[:topic_id].to_i) if url_slug.present? && topic[:topic_id].present? - # end - # end + # Add the following to permalink_normalizations for this to work: + # /forum\/.*?\/(\d*)\-.*/thread/\1 + + topics.each do |thread| + topic_id = "thread-#{thread["threadid"]}" + topic = topic_lookup_from_imported_post_id(topic_id) + if topic.present? + url_slug = "thread/#{thread["threadid"]}" if thread["title"].present? + Permalink.create(url: url_slug, topic_id: topic[:topic_id].to_i) if url_slug.present? && topic[:topic_id].present? + end + end end end @@ -388,8 +401,9 @@ EOM # find the uploaded file information from the db def find_upload(post, attachment_id) sql = "SELECT a.attachmentid attachment_id, a.userid user_id, a.filedataid file_id, a.filename filename, - a.caption caption + LENGTH(fd.filedata) AS dbsize, filedata, a.caption caption FROM #{TABLE_PREFIX}attachment a + LEFT JOIN #{TABLE_PREFIX}filedata fd ON fd.filedataid = a.filedataid WHERE a.attachmentid = #{attachment_id}" results = mysql_query(sql) @@ -399,13 +413,22 @@ EOM end filename = File.join(ATTACHMENT_DIR, row['user_id'].to_s.split('').join('/'), "#{row['file_id']}.attach") - unless File.exists?(filename) - puts "Attachment file doesn't exist: #{filename}" - return - end - real_filename = row['filename'] real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + + unless File.exists?(filename) + if row['dbsize'].to_i == 0 + puts "Attachment file #{row['filedataid']} doesn't exist" + return nil + end + + tmpfile = 'attach_' + row['filedataid'].to_s + filename = File.join('/tmp/', tmpfile) + File.open(filename, 'wb') { |f| + f.write(row['filedata']) + } + end + upload = create_upload(post.user.id, filename, real_filename) if upload.nil? || !upload.valid? @@ -609,7 +632,7 @@ EOM WHERE id IN (SELECT topic_id FROM closed_topic_ids) SQL - Topic.exec_sql(sql, closed_topic_ids) + DB.exec(sql, closed_topic_ids) end def post_process_posts @@ -620,8 +643,9 @@ EOM Post.find_each do |post| begin + old_raw = post.raw.dup new_raw = postprocess_post_raw(post.raw) - if new_raw != post.raw + if new_raw != old_raw post.raw = new_raw post.save end @@ -685,11 +709,8 @@ EOM # [MENTION][/MENTION] raw.gsub!(/\[mention\](.+?)\[\/mention\]/i) do - old_username = $1 - if @old_username_to_new_usernames.has_key?(old_username) - old_username = @old_username_to_new_usernames[old_username] - end - "@#{old_username}" + new_username = get_username_for_old_username($1) + "@#{new_username}" end # [FONT=blah] and [COLOR=blah] @@ -698,6 +719,7 @@ EOM raw.gsub! /\[COLOR=#.*?\](.*?)\[\/COLOR\]/im, '\1' raw.gsub! /\[SIZE=.*?\](.*?)\[\/SIZE\]/im, '\1' + raw.gsub! /\[SUP\](.*?)\[\/SUP\]/im, '\1' raw.gsub! /\[h=.*?\](.*?)\[\/h\]/im, '\1' # [CENTER]...[/CENTER] @@ -705,10 +727,18 @@ EOM # [INDENT]...[/INDENT] raw.gsub! /\[INDENT\](.*?)\[\/INDENT\]/im, '\1' - raw.gsub! /\[TABLE\](.*?)\[\/TABLE\]/im, '\1' - raw.gsub! /\[TR\](.*?)\[\/TR\]/im, '\1' - raw.gsub! /\[TD\](.*?)\[\/TD\]/im, '\1' - raw.gsub! /\[TD="?.*?"?\](.*?)\[\/TD\]/im, '\1' + + # Tables to MD + raw.gsub!(/\[TABLE.*?\](.*?)\[\/TABLE\]/im) { |t| + rows = $1.gsub!(/\s*\[TR\](.*?)\[\/TR\]\s*/im) { |r| + cols = $1.gsub! /\s*\[TD.*?\](.*?)\[\/TD\]\s*/im, '|\1' + "#{cols}|\n" + } + header, rest = rows.split "\n", 2 + c = header.count "|" + sep = "|---" * (c - 1) + "#{header}\n#{sep}|\n#{rest}\n" + } # [QUOTE]...[/QUOTE] raw.gsub!(/\[quote\](.+?)\[\/quote\]/im) { |quote| @@ -719,10 +749,8 @@ EOM # [QUOTE=]...[/QUOTE] raw.gsub!(/\[quote=([^;\]]+)\](.+?)\[\/quote\]/im) do old_username, quote = $1, $2 - if @old_username_to_new_usernames.has_key?(old_username) - old_username = @old_username_to_new_usernames[old_username] - end - "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n" + new_username = get_username_for_old_username(old_username) + "\n[quote=\"#{new_username}\"]\n#{quote}\n[/quote]\n" end # [YOUTUBE][/YOUTUBE] @@ -731,6 +759,9 @@ EOM # [VIDEO=youtube;]...[/VIDEO] raw.gsub!(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\n//youtu.be/#{$1}\n" } + # Fix uppercase B U and I tags + raw.gsub!(/(\[\/?[BUI]\])/i) { $1.downcase } + # More Additions .... # [spoiler=Some hidden stuff]SPOILER HERE!![/spoiler] @@ -760,16 +791,19 @@ EOM raw.gsub!(/\[quote=([^;]+);(\d+)\](.+?)\[\/quote\]/im) do old_username, post_id, quote = $1, $2, $3 - if @old_username_to_new_usernames.has_key?(old_username) - old_username = @old_username_to_new_usernames[old_username] - end + new_username = get_username_for_old_username(old_username) + + # There is a bug here when the first post in a topic is quoted. + # The first post in a topic does not have an post_custom_field referring to the post number, + # but it refers to thread-XXX instead, so this lookup fails miserably then. + # Fixing this would imply rewriting that logic completely. if topic_lookup = topic_lookup_from_imported_post_id(post_id) post_number = topic_lookup[:post_number] topic_id = topic_lookup[:topic_id] - "\n[quote=\"#{old_username},post:#{post_number},topic:#{topic_id}\"]\n#{quote}\n[/quote]\n" + "\n[quote=\"#{new_username},post:#{post_number},topic:#{topic_id}\"]\n#{quote}\n[/quote]\n" else - "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n" + "\n[quote=\"#{new_username}\"]\n#{quote}\n[/quote]\n" end end diff --git a/script/import_scripts/vbulletin5.rb b/script/import_scripts/vbulletin5.rb index da1ceea84b..36a46745b5 100644 --- a/script/import_scripts/vbulletin5.rb +++ b/script/import_scripts/vbulletin5.rb @@ -418,7 +418,7 @@ class ImportScripts::VBulletin < ImportScripts::Base WHERE id IN (SELECT topic_id FROM closed_topic_ids) SQL - Topic.exec_sql(sql, @closed_topic_ids) + DB.exec(sql, @closed_topic_ids) end def post_process_posts diff --git a/script/import_scripts/zendesk.rb b/script/import_scripts/zendesk.rb index 305431c7a5..15d44282b2 100644 --- a/script/import_scripts/zendesk.rb +++ b/script/import_scripts/zendesk.rb @@ -203,7 +203,7 @@ class ImportScripts::Zendesk < ImportScripts::Base end { - id: import_topic_id(row['id']), + id: row['id'], raw: normalize_raw(row['raw']), user_id: user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id, topic_id: topic[:topic_id], diff --git a/script/memory-analysis b/script/memory-analysis new file mode 100755 index 0000000000..616714043c --- /dev/null +++ b/script/memory-analysis @@ -0,0 +1,168 @@ +#!/usr/bin/env ruby + +require 'fileutils' +require 'pathname' +require 'tmpdir' +require 'json' +require 'set' + +def usage + STDERR.puts "Usage: memory-analysis [PID|DUMPFILE]" + exit 1 +end + +if ARGV.length != 1 + usage +end + +dumpfile = ARGV[0] + +if !File.exist?(dumpfile) + pid = dumpfile.to_i + usage if pid == 0 + + time = Time.now.utc + utc = time.strftime("%Y-%m-%d-%H-%M-%S") + dumpfile = "#{Pathname.new(Dir.tmpdir).realpath}/#{pid}_#{utc}.dump" + + puts "Dumping heap for pid #{pid} to #{dumpfile}" + puts + + `rbtrace -p #{pid} -e 'Thread.new{GC.start;require "objspace";io=File.open("#{dumpfile}", "w"); ObjectSpace.dump_all(output: io); io.close}'` + + old_size = 0 + + found = false + 20.times do + sleep 0.2 + found = File.exist?(dumpfile) + break if found + end + + if !found + STDERR.puts "Unable to find dumpfile #{dumpfile}, is rbtrace running properly, did you pick the right pid?" + usage + end + + while true + sleep 0.5 + size = File.size(dumpfile) + if size == old_size && size > 0 + break + end + old_size = size + end +end + +puts "Processing heap dump" + +class Stats + + def initialize + @classes = {} + @class_stats = {} + @type_stats = {} + @threads = Set.new + @thread_owners = {} + end + + def print + puts "Stats by Type" + puts "-" * 20 + puts + + @type_stats.sort_by { |_, (_, size)| -size }.each do |k, (count, size)| + puts "#{k} Count: #{count} Size: #{size}" + end + puts + + puts "Stats by Class" + puts "-" * 20 + @class_stats.sort_by { |_, (_, size)| -size }.each do |k, (count, size)| + puts "#{@classes[k] || k} Count: #{count} Size: #{size}" + end + + puts "Thread Stats" + puts "-" * 20 + @thread_owners.sort_by { |_ , count| -count }.each do |name, count| + puts "#{count} refs from #{name}" + end + end + + def thread_class + @thread_class ||= + begin + @classes.find do |addr, n| + n == "Thread" + end.first + end + end + + def detect_threads(line) + parsed = JSON.parse(line) + + if parsed["class"] == thread_class + @threads << parsed["address"] + end + end + + def detect_thread_owners(line) + parsed = JSON.parse(line) + + if refs = parsed["references"] + i = 0 + while i < refs.length + if @threads.include?(refs[i]) + klass = @classes[parsed["class"]] || "#{parsed["type"]} #{parsed["address"]}" + @thread_owners[klass] ||= 0 + @thread_owners[klass] += 1 + end + i += 1 + end + end + end + + def injest(line) + parsed = JSON.parse(line) + if parsed["type"] == "CLASS" + @classes[parsed["address"]] = parsed["name"] + end + + type_stat = @type_stats[parsed["type"]] ||= [0, 0] + + if klass = parsed["class"] + class_stat = @class_stats[parsed["class"]] ||= [0, 0] + class_stat[0] += 1 + class_stat[1] += parsed["memsize"] || 0 + end + + type_stat[0] += 1 + type_stat[1] += parsed["memsize"] || 0 + end +end + +def process_dumpfile(dumpfile) + stats = Stats.new + + File.open(dumpfile).each_line do |line| + stats.injest(line) + end + + puts "pass 1 done" + + File.open(dumpfile).each_line do |line| + stats.detect_threads(line) + end + + puts "pass 2 done" + + File.open(dumpfile).each_line do |line| + stats.detect_thread_owners(line) + end + + stats +end + +stats = process_dumpfile(dumpfile) + +stats.print diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index a4ca7ede22..951fc9d12d 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -27,7 +27,6 @@ describe ActiveRecord::ConnectionHandling do let(:postgresql_fallback_handler) { PostgreSQLFallbackHandler.instance } before do - skip("Figure out why this test leaks connections") postgresql_fallback_handler.initialized = true ['default', multisite_db].each do |db| @@ -37,7 +36,9 @@ describe ActiveRecord::ConnectionHandling do after do postgresql_fallback_handler.setup! - postgresql_fallback_handler.clear_connections + Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) + ActiveRecord::Base.unstub(:postgresql_connection) + ActiveRecord::Base.establish_connection end describe "#postgresql_fallback_connection" do @@ -54,7 +55,6 @@ describe ActiveRecord::ConnectionHandling do context 'when master server is down' do before do - @replica_connection = mock('replica_connection') end @@ -70,6 +70,11 @@ describe ActiveRecord::ConnectionHandling do end it 'should failover to a replica server' do + # erratically fails with: ActiveRecord::ConnectionTimeoutError: + # could not obtain a connection from the pool within 5.000 seconds (waited 5.000 seconds); all pooled connections were in use + # + skip("This test is failing erratically") + RailsMultisite::ConnectionManagement.stubs(:all_dbs).returns(['default', multisite_db]) postgresql_fallback_handler.expects(:verify_master).at_least(3) @@ -124,6 +129,8 @@ describe ActiveRecord::ConnectionHandling do expect(Discourse.readonly_mode?).to eq(false) expect(Sidekiq.paused?).to eq(false) + + # fails sometimes on this line! expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) expect(postgresql_fallback_handler.master_down?).to eq(nil) diff --git a/spec/components/concern/has_custom_fields_spec.rb b/spec/components/concern/has_custom_fields_spec.rb index 035e0260c5..1a55fc9f7c 100644 --- a/spec/components/concern/has_custom_fields_spec.rb +++ b/spec/components/concern/has_custom_fields_spec.rb @@ -4,8 +4,8 @@ describe HasCustomFields do context "custom_fields" do before do - Topic.exec_sql("create temporary table custom_fields_test_items(id SERIAL primary key)") - Topic.exec_sql("create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text)") + DB.exec("create temporary table custom_fields_test_items(id SERIAL primary key)") + DB.exec("create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text)") class CustomFieldsTestItem < ActiveRecord::Base include HasCustomFields @@ -17,8 +17,8 @@ describe HasCustomFields do end after do - Topic.exec_sql("drop table custom_fields_test_items") - Topic.exec_sql("drop table custom_fields_test_item_custom_fields") + DB.exec("drop table custom_fields_test_items") + DB.exec("drop table custom_fields_test_item_custom_fields") # import is making my life hard, we need to nuke this out of orbit des = ActiveSupport::DescendantsTracker.class_variable_get :@@direct_descendants @@ -75,7 +75,7 @@ describe HasCustomFields do # should be casted right after saving expect(test_item.custom_fields["a"]).to eq("0") - CustomFieldsTestItem.exec_sql("UPDATE custom_fields_test_item_custom_fields SET value='1' WHERE custom_fields_test_item_id=? AND name='a'", test_item.id) + DB.exec("UPDATE custom_fields_test_item_custom_fields SET value='1' WHERE custom_fields_test_item_id=? AND name='a'", test_item.id) # still the same, did not load expect(test_item.custom_fields["a"]).to eq("0") diff --git a/spec/components/concern/has_search_data_spec.rb b/spec/components/concern/has_search_data_spec.rb index 9cc3089f58..8c5441b246 100644 --- a/spec/components/concern/has_search_data_spec.rb +++ b/spec/components/concern/has_search_data_spec.rb @@ -3,8 +3,8 @@ require "rails_helper" describe HasSearchData do context "belongs to its model" do before do - Topic.exec_sql("create temporary table model_items(id SERIAL primary key)") - Topic.exec_sql("create temporary table model_item_search_data(model_item_id int primary key, search_data tsvector, raw_data text, locale text)") + DB.exec("create temporary table model_items(id SERIAL primary key)") + DB.exec("create temporary table model_item_search_data(model_item_id int primary key, search_data tsvector, raw_data text, locale text)") class ModelItem < ActiveRecord::Base has_one :model_item_search_data, dependent: :destroy @@ -16,8 +16,8 @@ describe HasSearchData do end after do - Topic.exec_sql("drop table model_items") - Topic.exec_sql("drop table model_item_search_data") + DB.exec("drop table model_items") + DB.exec("drop table model_item_search_data") # import is making my life hard, we need to nuke this out of orbit des = ActiveSupport::DescendantsTracker.class_variable_get :@@direct_descendants diff --git a/spec/components/concern/positionable_spec.rb b/spec/components/concern/positionable_spec.rb index d8433d6711..4ab010a538 100644 --- a/spec/components/concern/positionable_spec.rb +++ b/spec/components/concern/positionable_spec.rb @@ -12,11 +12,11 @@ describe Positionable do include Positionable end - Topic.exec_sql("create temporary table test_items(id int primary key, position int)") + DB.exec("create temporary table test_items(id int primary key, position int)") end after do - Topic.exec_sql("drop table test_items") + DB.exec("drop table test_items") # import is making my life hard, we need to nuke this out of orbit des = ActiveSupport::DescendantsTracker.class_variable_get :@@direct_descendants @@ -25,7 +25,7 @@ describe Positionable do it "can position stuff correctly" do 5.times do |i| - Topic.exec_sql("insert into test_items(id,position) values(#{i}, #{i})") + DB.exec("insert into test_items(id,position) values(#{i}, #{i})") end expect(positions).to eq([0, 1, 2, 3, 4]) diff --git a/spec/components/concern/searchable_spec.rb b/spec/components/concern/searchable_spec.rb index 365f2b16f8..5c55c7d1ff 100644 --- a/spec/components/concern/searchable_spec.rb +++ b/spec/components/concern/searchable_spec.rb @@ -3,8 +3,8 @@ require "rails_helper" describe Searchable do context "has search data" do before do - Topic.exec_sql("create temporary table searchable_records(id SERIAL primary key)") - Topic.exec_sql("create temporary table searchable_record_search_data(searchable_record_id int primary key, search_data tsvector, raw_data text, locale text)") + DB.exec("create temporary table searchable_records(id SERIAL primary key)") + DB.exec("create temporary table searchable_record_search_data(searchable_record_id int primary key, search_data tsvector, raw_data text, locale text)") class SearchableRecord < ActiveRecord::Base include Searchable @@ -17,8 +17,8 @@ describe Searchable do end after do - Topic.exec_sql("drop table searchable_records") - Topic.exec_sql("drop table searchable_record_search_data") + DB.exec("drop table searchable_records") + DB.exec("drop table searchable_record_search_data") # import is making my life hard, we need to nuke this out of orbit des = ActiveSupport::DescendantsTracker.class_variable_get :@@direct_descendants diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 103bbc4f79..0d1fa216a3 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -152,15 +152,15 @@ describe CookedPostProcessor do before do SiteSetting.max_image_height = 2000 SiteSetting.create_thumbnails = true - - Upload.expects(:get_from_url).returns(upload) FastImage.expects(:size).returns([1750, 2000]) - OptimizedImage.expects(:resize).returns(true) - - FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0) end it "generates overlay information" do + Upload.expects(:get_from_url).returns(upload) + OptimizedImage.expects(:resize).returns(true) + + FileStore::BaseStore.any_instance.expects(:get_depth_for).returns(0) + cpp.post_process_images expect(cpp.html).to match_html "

logo.png1750x2000 1.21 KB @@ -168,6 +168,31 @@ describe CookedPostProcessor do expect(cpp).to be_dirty end + describe 'when image is an svg' do + let(:post) do + Fabricate(:post, raw: '') + end + + it 'should not add lightbox' do + cpp.post_process_images + + expect(cpp.html).to match_html("

") + end + + describe 'when image src is an URL' do + let(:post) do + Fabricate(:post, raw: '') + end + + it 'should not add lightbox' do + SiteSetting.crawl_images = true + cpp.post_process_images + + expect(cpp.html).to match_html("

") + end + end + end + end context "with tall images" do diff --git a/spec/components/crawler_detection_spec.rb b/spec/components/crawler_detection_spec.rb index 65f811ef51..7fff4e80cc 100644 --- a/spec/components/crawler_detection_spec.rb +++ b/spec/components/crawler_detection_spec.rb @@ -2,47 +2,61 @@ require 'rails_helper' require_dependency 'crawler_detection' describe CrawlerDetection do + + def crawler!(s) + if (!CrawlerDetection.crawler?(s)) + raise "#{s} should be a crawler!" + end + end + + def not_crawler!(s) + if CrawlerDetection.crawler?(s) + raise "#{s} should not be a crawler!" + end + end + describe "crawler?" do it "can be amended via site settings" do SiteSetting.crawler_user_agents = 'Mooble|Kaboodle+*' - expect(CrawlerDetection.crawler?("Mozilla/5.0 Safari (compatible; Kaboodle+*/2.1; +http://www.google.com/bot.html)")).to eq(true) - expect(CrawlerDetection.crawler?("Mozilla/5.0 Safari (compatible; Mooble+*/2.1; +http://www.google.com/bot.html)")).to eq(true) - expect(CrawlerDetection.crawler?("Mozilla/5.0 Safari (compatible; Gooble+*/2.1; +http://www.google.com/bot.html)")).to eq(false) + + crawler! "Mozilla/5.0 Safari (compatible; Kaboodle+*/2.1; +http://www.google.com/bot.html)" + crawler! "Mozilla/5.0 Safari (compatible; Mooble+*/2.1; +http://www.google.com/bot.html)" + not_crawler! "Mozilla/5.0 Safari (compatible; Gooble+*/2.1; +http://www.google.com/bot.html)" end it "returns true for crawler user agents" do # https://support.google.com/webmasters/answer/1061943?hl=en - expect(described_class.crawler?("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")).to eq(true) - expect(described_class.crawler?("Googlebot/2.1 (+http://www.google.com/bot.html)")).to eq(true) - expect(described_class.crawler?("Googlebot-News")).to eq(true) - expect(described_class.crawler?("Googlebot-Image/1.0")).to eq(true) - expect(described_class.crawler?("Googlebot-Video/1.0")).to eq(true) - expect(described_class.crawler?("(compatible; Googlebot-Mobile/2.1; +http://www.google.com/bot.html)")).to eq(true) - expect(described_class.crawler?("Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")).to eq(true) - expect(described_class.crawler?("(compatible; Mediapartners-Google/2.1; +http://www.google.com/bot.html)")).to eq(true) - expect(described_class.crawler?("Mediapartners-Google")).to eq(true) - expect(described_class.crawler?("AdsBot-Google (+http://www.google.com/adsbot.html)")).to eq(true) - expect(described_class.crawler?("Twitterbot")).to eq(true) - expect(described_class.crawler?("facebookexternalhit/1.1 (+http(s)://www.facebook.com/externalhit_uatext.php)")).to eq(true) - expect(described_class.crawler?("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)")).to eq(true) - expect(described_class.crawler?("Baiduspider+(+http://www.baidu.com/search/spider.htm)")).to eq(true) - expect(described_class.crawler?("Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)")).to eq(true) - - expect(described_class.crawler?("DiscourseAPI Ruby Gem 0.19.0")).to eq(true) - expect(described_class.crawler?("Pingdom.com_bot_version_1.4_(http://www.pingdom.com/)")).to eq(true) - expect(described_class.crawler?("LogicMonitor SiteMonitor/1.0")).to eq(true) - expect(described_class.crawler?("Java/1.8.0_151")).to eq(true) - expect(described_class.crawler?("Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)")).to eq(true) + crawler! "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" + crawler! "Googlebot/2.1 (+http://www.google.com/bot.html)" + crawler! "Googlebot-News" + crawler! "Googlebot-Image/1.0" + crawler! "Googlebot-Video/1.0" + crawler! "(compatible; Googlebot-Mobile/2.1; +http://www.google.com/bot.html)" + crawler! "Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" + crawler! "(compatible; Mediapartners-Google/2.1; +http://www.google.com/bot.html)" + crawler! "Mediapartners-Google" + crawler! "AdsBot-Google (+http://www.google.com/adsbot.html)" + crawler! "Twitterbot" + crawler! "facebookexternalhit/1.1 (+http(s)://www.facebook.com/externalhit_uatext.php)" + crawler! "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)" + crawler! "Baiduspider+(+http://www.baidu.com/search/spider.htm)" + crawler! "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)" + crawler! "DiscourseAPI Ruby Gem 0.19.0" + crawler! "Pingdom.com_bot_version_1.4_(http://www.pingdom.com/)" + crawler! "LogicMonitor SiteMonitor/1.0" + crawler! "Java/1.8.0_151" + crawler! "Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)" end it "returns false for non-crawler user agents" do - expect(described_class.crawler?("Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36")).to eq(false) - expect(described_class.crawler?("Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko")).to eq(false) - expect(described_class.crawler?("Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)")).to eq(false) - expect(described_class.crawler?("Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25")).to eq(false) - expect(described_class.crawler?("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0")).to eq(false) - expect(described_class.crawler?("Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")).to eq(false) + not_crawler! "Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36" + not_crawler! "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko" + not_crawler! "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)" + not_crawler! "Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25" + not_crawler! "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0" + not_crawler! "Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30" + not_crawler! "Mozilla/5.0 (Linux; Android 6.0; CUBOT DINOSAUR Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Mobile Safari/537.36+" end end diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb index b9ef0df7d9..b8030d84f7 100644 --- a/spec/components/discourse_spec.rb +++ b/spec/components/discourse_spec.rb @@ -177,6 +177,7 @@ describe Discourse do it "returns true when user enabled readonly mode key is present in redis" do Discourse.enable_readonly_mode(user_readonly_mode_key) expect(Discourse.readonly_mode?).to eq(true) + expect(Discourse.readonly_mode?(readonly_mode_key)).to eq(false) Discourse.disable_readonly_mode(user_readonly_mode_key) expect(Discourse.readonly_mode?).to eq(false) @@ -225,4 +226,41 @@ describe Discourse do end end + context '#deprecate' do + + class FakeLogger + attr_reader :warnings + def warn(m) + @warnings ||= [] + @warnings << m + end + end + + def old_method(m) + Discourse.deprecate(m) + end + + def old_method_caller(m) + old_method(m) + end + + before do + @orig_logger = Rails.logger + Rails.logger = @fake_logger = FakeLogger.new + end + + after do + Rails.logger = @orig_logger + end + + it 'can deprecate usage' do + k = SecureRandom.hex + expect(old_method_caller(k)).to include("old_method_caller") + expect(old_method_caller(k)).to include("discourse_spec") + expect(old_method_caller(k)).to include(k) + + expect(@fake_logger.warnings).to eq([old_method_caller(k)]) + end + end + end diff --git a/spec/components/email/message_builder_spec.rb b/spec/components/email/message_builder_spec.rb index 9ed9200d51..9283f87262 100644 --- a/spec/components/email/message_builder_spec.rb +++ b/spec/components/email/message_builder_spec.rb @@ -247,17 +247,6 @@ describe Email::MessageBuilder do end - context "PM multiple participants" do - let(:pm_multiple) { Email::MessageBuilder.new(to_address, - body: 'hello world', - private_reply: true, - participants: "user1, user2") } - - it "lists participants out" do - expect(pm_multiple.body).to match('hello world\nuser1, user2') - end - end - context "from field" do it "has the default from" do diff --git a/spec/components/email/sender_spec.rb b/spec/components/email/sender_spec.rb index 7c73e7e2fb..65838ab1d0 100644 --- a/spec/components/email/sender_spec.rb +++ b/spec/components/email/sender_spec.rb @@ -3,18 +3,41 @@ require 'email/sender' describe Email::Sender do - it "doesn't deliver mail when mails are disabled" do - SiteSetting.disable_emails = true - Mail::Message.any_instance.expects(:deliver_now).never - message = Mail::Message.new(to: "hello@world.com" , body: "hello") - expect(Email::Sender.new(message, :hello).send).to eq(nil) - end + context "disable_emails is enabled" do + let(:user) { Fabricate(:user) } + let(:moderator) { Fabricate(:moderator) } - it "delivers mail when mails are disabled but the email_type is admin_login" do - SiteSetting.disable_emails = true - Mail::Message.any_instance.expects(:deliver_now).once - message = Mail::Message.new(to: "hello@world.com" , body: "hello") - Email::Sender.new(message, :admin_login).send + context "disable_emails is enabled for everyone" do + before { SiteSetting.disable_emails = "yes" } + + it "doesn't deliver mail when mails are disabled" do + Mail::Message.any_instance.expects(:deliver_now).never + message = Mail::Message.new(to: moderator.email , body: "hello") + expect(Email::Sender.new(message, :hello).send).to eq(nil) + end + + it "delivers mail when mails are disabled but the email_type is admin_login" do + Mail::Message.any_instance.expects(:deliver_now).once + message = Mail::Message.new(to: moderator.email , body: "hello") + Email::Sender.new(message, :admin_login).send + end + end + + context "disable_emails is enabled for non-staff users" do + before { SiteSetting.disable_emails = "non-staff" } + + it "doesn't deliver mail to normal user" do + Mail::Message.any_instance.expects(:deliver_now).never + message = Mail::Message.new(to: user.email, body: "hello") + expect(Email::Sender.new(message, :hello).send).to eq(nil) + end + + it "delivers mail to staff user" do + Mail::Message.any_instance.expects(:deliver_now).once + message = Mail::Message.new(to: moderator.email, body: "hello") + Email::Sender.new(message, :hello).send + end + end end it "doesn't deliver mail when the message is of type NullMail" do diff --git a/spec/components/file_store/base_store_spec.rb b/spec/components/file_store/base_store_spec.rb index 89f0a105da..20d898ee36 100644 --- a/spec/components/file_store/base_store_spec.rb +++ b/spec/components/file_store/base_store_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require_dependency 'file_store/base_store' RSpec.describe FileStore::BaseStore do let(:upload) { Fabricate(:upload, id: 9999, sha1: Digest::SHA1.hexdigest('9999')) } diff --git a/spec/components/freedom_patches/pool_drainer_spec.rb b/spec/components/freedom_patches/pool_drainer_spec.rb deleted file mode 100644 index 32fc98dc85..0000000000 --- a/spec/components/freedom_patches/pool_drainer_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'rails_helper' - -describe 'pool drainer' do - let(:pool) do - ActiveRecord::Base.connection_pool - end - - it 'can correctly drain the connection pool' do - pool.drain - old = pool.connections.length - expect(old).to eq(1) - - Thread.new do - conn = pool.checkout - pool.checkin conn - end.join - - expect(pool.connections.length).to eq(old + 1) - pool.drain - expect(pool.connections.length).to eq(old) - end - - it 'can drain with idle time setting' do - pool.drain - old = pool.connections.length - expect(old).to eq(1) - - Thread.new do - conn = pool.checkout - pool.checkin conn - end.join - - expect(pool.connections.length).to eq(old + 1) - pool.drain(1.minute) - expect(pool.connections.length).to eq(old + 1) - - # make sure we don't corrupt internal state - 20.times do - conn = pool.checkout - pool.checkin conn - pool.drain - end - - end - -end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index d849d583f5..f0d2b03ec2 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -26,21 +26,59 @@ describe Guardian do expect { Guardian.new(user) }.not_to raise_error end + describe "link_posting_access" do + it "is none for anonymous users" do + expect(Guardian.new.link_posting_access).to eq('none') + end + + it "is full for regular users" do + expect(Guardian.new(user).link_posting_access).to eq('full') + end + + it "is none for a user of a low trust level" do + user.trust_level = 0 + SiteSetting.min_trust_to_post_links = 1 + expect(Guardian.new(user).link_posting_access).to eq('none') + end + + it "is limited for a user of a low trust level with a whitelist" do + SiteSetting.whitelisted_link_domains = 'example.com' + user.trust_level = 0 + SiteSetting.min_trust_to_post_links = 1 + expect(Guardian.new(user).link_posting_access).to eq('limited') + end + end + describe "can_post_link?" do + let(:host) { "discourse.org" } + it "returns false for anonymous users" do - expect(Guardian.new.can_post_link?).to eq(false) + expect(Guardian.new.can_post_link?(host: host)).to eq(false) end it "returns true for a regular user" do - expect(Guardian.new(user).can_post_link?).to eq(true) + expect(Guardian.new(user).can_post_link?(host: host)).to eq(true) end it "supports customization by site setting" do user.trust_level = 0 SiteSetting.min_trust_to_post_links = 0 - expect(Guardian.new(user).can_post_link?).to eq(true) + expect(Guardian.new(user).can_post_link?(host: host)).to eq(true) SiteSetting.min_trust_to_post_links = 1 - expect(Guardian.new(user).can_post_link?).to eq(false) + expect(Guardian.new(user).can_post_link?(host: host)).to eq(false) + end + + describe "whitelisted host" do + before do + SiteSetting.whitelisted_link_domains = host + end + + it "allows a new user to post the link to the host" do + user.trust_level = 0 + SiteSetting.min_trust_to_post_links = 1 + expect(Guardian.new(user).can_post_link?(host: host)).to eq(true) + expect(Guardian.new(user).can_post_link?(host: 'another-host.com')).to eq(false) + end end end @@ -2243,6 +2281,10 @@ describe Guardian do it "returns false if title is from a group the user doesn't belong to" do expect(Guardian.new(user).can_grant_title?(user, group.title)).to eq(false) end + + it "returns true if the title is set to an empty string" do + expect(Guardian.new(user).can_grant_title?(user, '')).to eq(true) + end end end diff --git a/spec/components/migration/column_dropper_spec.rb b/spec/components/migration/column_dropper_spec.rb index 9a0bd536e2..b83aa32fc7 100644 --- a/spec/components/migration/column_dropper_spec.rb +++ b/spec/components/migration/column_dropper_spec.rb @@ -4,7 +4,7 @@ require_dependency 'migration/column_dropper' RSpec.describe Migration::ColumnDropper do def has_column?(table, column) - ActiveRecord::Base.exec_sql(<<~SQL, table: table, column: column).to_a.length == 1 + DB.exec(<<~SQL, table: table, column: column) == 1 SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE @@ -15,7 +15,7 @@ RSpec.describe Migration::ColumnDropper do end def update_first_migration_date(created_at) - ActiveRecord::Base.exec_sql(<<~SQL, created_at: created_at) + DB.exec(<<~SQL, created_at: created_at) UPDATE schema_migration_details SET created_at = :created_at WHERE id = (SELECT MIN(id) @@ -25,15 +25,13 @@ RSpec.describe Migration::ColumnDropper do describe ".drop" do let(:migration_name) do - ActiveRecord::Base - .exec_sql("SELECT name FROM schema_migration_details ORDER BY id DESC LIMIT 1") - .getvalue(0, 0) + DB.query_single("SELECT name FROM schema_migration_details ORDER BY id DESC LIMIT 1").first end before do - Topic.exec_sql "ALTER TABLE topics ADD COLUMN junk int" + DB.exec "ALTER TABLE topics ADD COLUMN junk int" - ActiveRecord::Base.exec_sql(<<~SQL, name: migration_name, created_at: 15.minutes.ago) + DB.exec(<<~SQL, name: migration_name, created_at: 15.minutes.ago) UPDATE schema_migration_details SET created_at = :created_at WHERE name = :name @@ -114,7 +112,7 @@ RSpec.describe Migration::ColumnDropper do let(:table_name) { "table_with_readonly_column" } before do - ActiveRecord::Base.exec_sql <<~SQL + DB.exec <<~SQL CREATE TABLE #{table_name} (topic_id INTEGER, email TEXT); INSERT INTO #{table_name} (topic_id, email) @@ -127,16 +125,14 @@ RSpec.describe Migration::ColumnDropper do after do ActiveRecord::Base.connection.reset! - ActiveRecord::Base.exec_sql <<~SQL + DB.exec <<~SQL DROP TABLE IF EXISTS #{table_name}; DROP TRIGGER IF EXISTS #{table_name}_email_readonly ON #{table_name}; SQL end it 'should be droppable' do - name = Topic - .exec_sql("SELECT name FROM schema_migration_details LIMIT 1") - .getvalue(0, 0) + name = DB.query_single("SELECT name FROM schema_migration_details LIMIT 1").first dropped_proc_called = false Migration::ColumnDropper.drop( @@ -152,7 +148,7 @@ RSpec.describe Migration::ColumnDropper do end it 'should prevent updates to the readonly column' do expect do - ActiveRecord::Base.connection.raw_connection.exec <<~SQL + DB.exec <<~SQL UPDATE #{table_name} SET email = 'testing@email.com' WHERE topic_id = 1; @@ -164,7 +160,7 @@ RSpec.describe Migration::ColumnDropper do end it 'should allow updates to the other columns' do - ActiveRecord::Base.exec_sql <<~SQL + DB.exec <<~SQL UPDATE #{table_name} SET topic_id = 2 WHERE topic_id = 1 @@ -188,14 +184,14 @@ RSpec.describe Migration::ColumnDropper do end it 'should allow insertions to the other columns' do - ActiveRecord::Base.exec_sql <<~SQL + DB.exec <<~SQL INSERT INTO #{table_name} (topic_id) VALUES (2); SQL expect( - ActiveRecord::Base.exec_sql("SELECT * FROM #{table_name} WHERE topic_id = 2;").values - ).to include([2, nil]) + DB.query_single("SELECT topic_id FROM #{table_name} WHERE topic_id = 2") + ).to eq([2]) end end end diff --git a/spec/components/migration/safe_migrate_spec.rb b/spec/components/migration/safe_migrate_spec.rb index 1be7f7c86d..15659e4b90 100644 --- a/spec/components/migration/safe_migrate_spec.rb +++ b/spec/components/migration/safe_migrate_spec.rb @@ -21,6 +21,11 @@ describe Migration::SafeMigrate do $stdout = old_stdout end + def migrate_up(path) + migrations = ActiveRecord::MigrationContext.new(path).migrations + ActiveRecord::Migrator.new(:up, migrations, migrations.first.version).run + end + it "bans all table removal" do Migration::SafeMigrate.enable! @@ -28,7 +33,7 @@ describe Migration::SafeMigrate do output = capture_stdout do expect(lambda do - ActiveRecord::Migrator.up([path]) + migrate_up(path) end).to raise_error(StandardError) end @@ -45,14 +50,14 @@ describe Migration::SafeMigrate do output = capture_stdout do expect(lambda do - ActiveRecord::Migrator.up([path]) + migrate_up(path) end).to raise_error(StandardError) end - expect(output).to include("TableDropper") - expect { User.first }.not_to raise_error expect(User.first).not_to eq(nil) + + expect(output).to include("TableDropper") end it "bans all column removal" do @@ -62,7 +67,7 @@ describe Migration::SafeMigrate do output = capture_stdout do expect(lambda do - ActiveRecord::Migrator.up([path]) + migrate_up(path) end).to raise_error(StandardError) end @@ -79,7 +84,7 @@ describe Migration::SafeMigrate do output = capture_stdout do expect(lambda do - ActiveRecord::Migrator.up([path]) + migrate_up(path) end).to raise_error(StandardError) end @@ -96,7 +101,7 @@ describe Migration::SafeMigrate do path = File.expand_path "#{Rails.root}/spec/fixtures/migrate/drop_table" output = capture_stdout do - ActiveRecord::Migrator.up([path]) + migrate_up(path) end expect(output).to include("drop_table(:users)") diff --git a/spec/components/migration/table_dropper_spec.rb b/spec/components/migration/table_dropper_spec.rb index 0b798f82bc..7db3c64f1d 100644 --- a/spec/components/migration/table_dropper_spec.rb +++ b/spec/components/migration/table_dropper_spec.rb @@ -11,11 +11,11 @@ describe Migration::TableDropper do table_name = '#{table_name}' SQL - ActiveRecord::Base.exec_sql(sql).to_a.length > 0 + DB.exec(sql) > 0 end def update_first_migration_date(created_at) - ActiveRecord::Base.exec_sql(<<~SQL, created_at: created_at) + DB.exec(<<~SQL, created_at: created_at) UPDATE schema_migration_details SET created_at = :created_at WHERE id = (SELECT MIN(id) @@ -24,19 +24,17 @@ describe Migration::TableDropper do end def create_new_table - ActiveRecord::Base.exec_sql "CREATE TABLE table_with_new_name (topic_id INTEGER)" + DB.exec "CREATE TABLE table_with_new_name (topic_id INTEGER)" end let(:migration_name) do - ActiveRecord::Base - .exec_sql("SELECT name FROM schema_migration_details ORDER BY id DESC LIMIT 1") - .getvalue(0, 0) + DB.query_single("SELECT name FROM schema_migration_details ORDER BY id DESC LIMIT 1").first end before do - ActiveRecord::Base.exec_sql "CREATE TABLE table_with_old_name (topic_id INTEGER)" + DB.exec "CREATE TABLE table_with_old_name (topic_id INTEGER)" - ActiveRecord::Base.exec_sql(<<~SQL, name: migration_name, created_at: 15.minutes.ago) + DB.exec(<<~SQL, name: migration_name, created_at: 15.minutes.ago) UPDATE schema_migration_details SET created_at = :created_at WHERE name = :name diff --git a/spec/components/plugin/instance_spec.rb b/spec/components/plugin/instance_spec.rb index b791a7d8f8..44456c818a 100644 --- a/spec/components/plugin/instance_spec.rb +++ b/spec/components/plugin/instance_spec.rb @@ -116,6 +116,15 @@ describe Plugin::Instance do end end + context "#add_report" do + it "adds a report" do + plugin = Plugin::Instance.new nil, "/tmp/test.rb" + plugin.add_report("readers") {} + + expect(Report.respond_to?(:report_readers)).to eq(true) + end + end + context "activate!" do it "can activate plugins correctly" do plugin = Plugin::Instance.new diff --git a/spec/components/post_revisor_spec.rb b/spec/components/post_revisor_spec.rb index 3e6efb0a6f..4734d94cec 100644 --- a/spec/components/post_revisor_spec.rb +++ b/spec/components/post_revisor_spec.rb @@ -86,7 +86,7 @@ describe PostRevisor do let(:post) { Fabricate(:post, post_args) } let(:first_version_at) { post.last_version_at } - subject { described_class.new(post) } + subject { PostRevisor.new(post) } describe 'with the same body' do it "doesn't change version" do @@ -402,7 +402,7 @@ describe PostRevisor do describe "topic excerpt" do it "topic excerpt is updated only if first post is revised" do - revisor = described_class.new(post) + revisor = PostRevisor.new(post) first_post = topic.posts.by_post_number.first expect { revisor.revise!(first_post.user, { raw: 'Edit the first post' }, revised_at: first_post.updated_at + 10.seconds) @@ -410,7 +410,7 @@ describe PostRevisor do }.to change { topic.excerpt } second_post = Fabricate(:post, post_args.merge(post_number: 2, topic_id: topic.id)) expect { - described_class.new(second_post).revise!(second_post.user, raw: 'Edit the 2nd post') + PostRevisor.new(second_post).revise!(second_post.user, raw: 'Edit the 2nd post') topic.reload }.to_not change { topic.excerpt } end @@ -423,10 +423,10 @@ describe PostRevisor do end context "#publish_changes" do - let!(:post) { Fabricate(:post, topic_id: topic.id) } + let!(:post) { Fabricate(:post, topic: topic) } it "should publish topic changes to clients" do - revisor = described_class.new(topic.ordered_posts.first, topic) + revisor = PostRevisor.new(topic.ordered_posts.first, topic) message = MessageBus.track_publish("/topic/#{topic.id}") do revisor.revise!(newuser, title: 'this is a test topic') diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 62a02b74fc..e1a51ec510 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -628,6 +628,11 @@ describe PrettyText do html = "

Check out this guy.

" expect { described_class.format_for_email(html, post) }.to_not raise_error end + + it "doesn't change mailto" do + html = "

Contact me at this address.

" + expect(PrettyText.format_for_email(html, post)).to eq(html) + end end it 'Is smart about linebreaks and IMG tags' do @@ -852,15 +857,15 @@ describe PrettyText do expect(cooked).to include(element) end - cooked = PrettyText.cook("[`a` #known::tag here](http://somesite.com)") + cooked = PrettyText.cook("[`a` #known::tag here](http://example.com)") html = <<~HTML -

a #known::tag here

+

a #known::tag here

HTML expect(cooked).to eq(html.strip) - cooked = PrettyText.cook("`a` #known::tag here") + cooked = PrettyText.cook("`a` #known::tag here") expect(cooked).to eq(html.strip) diff --git a/spec/components/rate_limiter/limit_exceeded_spec.rb b/spec/components/rate_limiter/limit_exceeded_spec.rb new file mode 100644 index 0000000000..f913add176 --- /dev/null +++ b/spec/components/rate_limiter/limit_exceeded_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe RateLimiter::LimitExceeded do + describe '#description' do + it 'should return the right description' do + [ + [3, I18n.t("rate_limiter.short_time")], + [59, I18n.t("rate_limiter.seconds", count: 59)], + [3599, I18n.t("rate_limiter.minutes", count: 59)], + [7000, I18n.t("rate_limiter.hours", count: 1)] + ].each do |available_in, time_left| + + expect(described_class.new(available_in).description).to eq(I18n.t( + "rate_limiter.too_many_requests", + time_left: time_left + )) + end + end + end +end diff --git a/spec/components/scheduler/manager_spec.rb b/spec/components/scheduler/manager_spec.rb index 4e96014726..aeaa9a3e64 100644 --- a/spec/components/scheduler/manager_spec.rb +++ b/spec/components/scheduler/manager_spec.rb @@ -61,7 +61,11 @@ describe Scheduler::Manager do before do expect(ActiveRecord::Base.connection_pool.connections.length).to eq(1) @thread_count = Thread.list.count - @thread_ids = Thread.list.map { |t| t.object_id } + + @backtraces = {} + Thread.list.each do |t| + @backtraces[t.object_id] = t.backtrace + end end after do @@ -81,11 +85,17 @@ describe Scheduler::Manager do on_thread_mismatch = lambda do current = Thread.list.map { |t| t.object_id } - extra = current - @thread_ids - missing = @thread_ids - current + old_threads = @backtraces.keys + extra = current - old_threads + + missing = old_threads - current if missing.length > 0 STDERR.puts "\nMissing Threads #{missing.length} thread/s" + missing.each do |id| + STDERR.puts @backtraces[id] + STDERR.puts + end end if extra.length > 0 diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index e293a87fd2..9ad75bf58c 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -895,7 +895,7 @@ describe Search do str << "page page on Atmosphere](https://atmospherejs.com/grigio/babel)xxx: aaa.js:222 aaa'\"bbb" ts_query = Search.ts_query(term: str, ts_config: "simple") - Post.exec_sql("SELECT to_tsvector('bbb') @@ " << ts_query) + DB.exec("SELECT to_tsvector('bbb') @@ " << ts_query) end context '#word_to_date' do @@ -964,7 +964,7 @@ describe Search do context 'in:title' do it 'allows for search in title' do topic = Fabricate(:topic, title: 'I am testing a title search') - _post = Fabricate(:post, topic_id: topic.id, raw: 'this is the first post') + _post = Fabricate(:post, topic: topic, raw: 'this is the first post') results = Search.execute('title in:title') expect(results.posts.length).to eq(1) diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 4171b9db4c..1e67d99a29 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -433,13 +433,13 @@ describe SiteSettingExtension do settings.refresh! expect { settings.set("provider", "haxxed") - }.to raise_error(ArgumentError) + }.to raise_error(Discourse::InvalidParameters) end end describe ".set_and_log" do before do - settings.setting(:s3_secret_access_key, "old_secret_key") + settings.setting(:s3_secret_access_key, "old_secret_key", secret: true) settings.setting(:title, "Discourse v1") settings.refresh! end @@ -447,7 +447,7 @@ describe SiteSettingExtension do it "raises an error when set for an invalid setting name" do expect { settings.set_and_log("provider", "haxxed") - }.to raise_error(ArgumentError) + }.to raise_error(Discourse::InvalidParameters) end it "scrubs secret setting values from logs" do @@ -505,6 +505,15 @@ describe SiteSettingExtension do end describe "shadowed_by_global" do + + context "default_locale" do + it "supports adding a default locale via a global" do + global_setting :default_locale, 'zh_CN' + settings.default_locale = 'en' + expect(settings.default_locale).to eq('zh_CN') + end + end + context "without global setting" do before do settings.setting(:trout_api_key, 'evil', shadowed_by_global: true) @@ -592,6 +601,27 @@ describe SiteSettingExtension do end end + describe "secret" do + before do + settings.setting(:superman_identity, 'Clark Kent', secret: true) + settings.refresh! + end + + it "is in the `secret_settings` collection" do + expect(settings.secret_settings.include?(:superman_identity)).to eq(true) + end + + it "can be retrieved" do + expect(settings.superman_identity).to eq("Clark Kent") + end + + it "is present in all_settings by default" do + secret_setting = settings.all_settings.find { |s| s[:setting] == :superman_identity } + expect(secret_setting).to be_present + expect(secret_setting[:secret]).to eq(true) + end + end + describe 'locale default overrides are respected' do before do settings.setting(:test_override, 'default', locale_default: { zh_CN: 'cn' }) diff --git a/spec/components/site_settings/defaults_provider_spec.rb b/spec/components/site_settings/defaults_provider_spec.rb index e19b369f2b..e4ca9a9109 100644 --- a/spec/components/site_settings/defaults_provider_spec.rb +++ b/spec/components/site_settings/defaults_provider_spec.rb @@ -26,20 +26,7 @@ describe SiteSettings::DefaultsProvider do new_settings(provider_local) end - describe 'inserts default_locale into refresh' do - it 'when initialize' do - expect(settings.refresh_settings.include?(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY)).to be_truthy - end - end - describe '.db_all' do - it 'collects values from db except default locale' do - settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, - 'en', - SiteSetting.types[:string]) - expect(settings.defaults.db_all).to eq([]) - end - it 'can collect values from db' do settings.provider.save('try_a', 1, SiteSetting.types[:integer]) settings.provider.save('try_b', 2, SiteSetting.types[:integer]) @@ -55,11 +42,9 @@ describe SiteSettings::DefaultsProvider do end describe '.all' do - it 'returns all values according to the current locale' do + it 'returns all values according to locale' do expect(settings.defaults.all).to eq(test_override: 'default', test_default: 'test') - settings.defaults.site_locale = 'zh_CN' - settings.defaults.refresh_site_locale! - expect(settings.defaults.all).to eq(test_override: 'cn', test_default: 'test') + expect(settings.defaults.all('zh_CN')).to eq(test_override: 'cn', test_default: 'test') end end @@ -72,11 +57,6 @@ describe SiteSettings::DefaultsProvider do expect(settings.defaults.get('test_override')).to eq 'default' end - it 'returns the default value according to current locale' do - expect(settings.defaults.get(:test_override)).to eq 'default' - settings.defaults.site_locale = 'zh_CN' - expect(settings.defaults.get(:test_override)).to eq 'cn' - end end describe '.set_regardless_of_locale' do @@ -85,8 +65,7 @@ describe SiteSettings::DefaultsProvider do it 'sets the default value to a site setting regardless the locale' do settings.defaults.set_regardless_of_locale(:test_override, val) expect(settings.defaults.get(:test_override)).to eq val - settings.defaults.site_locale = 'zh_CN' - expect(settings.defaults.get(:test_override)).to eq val + expect(settings.defaults.get(:test_override, 'zh_CN')).to eq val end it 'handles the string' do @@ -111,143 +90,13 @@ describe SiteSettings::DefaultsProvider do }.to raise_error(Discourse::InvalidParameters) end end - - describe '.each' do - it 'yields the pair of site settings' do - expect { |b| settings.defaults.each(&b) }.to yield_successive_args([:test_override, 'default'], [:test_default, 'test']) - settings.defaults.site_locale = 'zh_CN' - expect { |b| settings.defaults.each(&b) }.to yield_successive_args([:test_override, 'cn'], [:test_default, 'test']) - end - end - end - - describe '.site_locale' do - it 'returns the current site locale' do - expect(settings.defaults.site_locale).to eq 'en' - end - - context 'when locale is set in the db' do - let(:db_val) { 'zr' } - let(:global_val) { 'gr' } - - before do - settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, - db_val, - SiteSetting.types[:string]) - settings.defaults.refresh_site_locale! - end - - it 'should load from database' do - expect(settings.defaults.site_locale).to eq db_val - end - - it 'prioritizes GlobalSetting than value from db' do - GlobalSetting.stubs(:default_locale).returns(global_val) - settings.defaults.refresh_site_locale! - expect(settings.defaults.site_locale).to eq global_val - end - - it 'ignores blank GlobalSetting' do - GlobalSetting.stubs(:default_locale).returns('') - settings.defaults.refresh_site_locale! - expect(settings.defaults.site_locale).to eq db_val - end - end - - end - - describe '.site_locale=' do - it 'should store site locale in a distributed cache' do - expect(settings.defaults.class.class_variable_get(:@@site_locales)) - .to be_a(DistributedCache) - end - - it 'changes and store the current site locale' do - settings.defaults.site_locale = 'zh_CN' - - expect(settings.defaults.site_locale).to eq('zh_CN') - end - - it 'changes and store the current site locale' do - expect { settings.defaults.site_locale = 'random' }.to raise_error(Discourse::InvalidParameters) - expect(settings.defaults.site_locale).to eq 'en' - end - - it "don't change when it's shadowed" do - GlobalSetting.stubs(:default_locale).returns('shadowed') - settings.defaults.site_locale = 'zh_CN' - expect(settings.defaults.site_locale).to eq 'shadowed' - end - - it 'refresh_site_locale! when called' do - settings.defaults.expects(:refresh_site_locale!) - settings.defaults.site_locale = 'zh_CN' - end - - it 'refreshes the client when changed' do - Discourse.expects(:request_refresh!).once - settings.defaults.site_locale = 'zh_CN' - end - - it "doesn't refresh the client when changed" do - Discourse.expects(:request_refresh!).never - settings.defaults.site_locale = 'en' - end - end - - describe '.locale_setting_hash' do - it 'returns the hash for client display' do - result = settings.defaults.locale_setting_hash - - expect(result[:setting]).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY) - expect(result[:default]).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) - expect(result[:type]).to eq(SiteSetting.types[SiteSetting.types[:enum]]) - expect(result[:preview]).to be_nil - expect(result[:value]).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) - expect(result[:category]).to eq(SiteSettings::DefaultsProvider::DEFAULT_CATEGORY) - expect(result[:valid_values]).to eq(LocaleSiteSetting.values) - expect(result[:translate_names]).to eq(LocaleSiteSetting.translate_names?) - expect(result[:description]).not_to be_nil - end end describe '.load_setting' do - it 'adds a setting to the cache' do - settings.defaults.load_setting('new_a', 1) + it 'adds a setting to the cache correctly' do + settings.defaults.load_setting('new_a', 1, zh_CN: 7) expect(settings.defaults[:new_a]).to eq 1 - end - - it 'takes care of locale default' do - settings.defaults.load_setting(:new_b, 1, locale_default: { zh_CN: 2, zh_TW: 2 }) - expect(settings.defaults[:new_b]).to eq 1 - end - end - - describe '.refresh_site_locale!' do - it 'loads the change to locale' do - expect(settings.defaults.site_locale).to eq 'en' - settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, - 'zh_CN', - SiteSetting.types[:string]) - settings.defaults.refresh_site_locale! - expect(settings.defaults.site_locale).to eq 'zh_CN' - end - - it 'loads from GlobalSettings' do - expect(settings.defaults.site_locale).to eq 'en' - GlobalSetting.stubs(:default_locale).returns('fr') - settings.defaults.refresh_site_locale! - expect(settings.defaults.site_locale).to eq 'fr' - end - - it 'prioritized GlobalSettings than db' do - expect(settings.defaults.site_locale).to eq 'en' - settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, - 'zh_CN', - SiteSetting.types[:string]) - GlobalSetting.stubs(:default_locale).returns('fr') - settings.defaults.refresh_site_locale! - expect(settings.defaults.site_locale).to eq 'fr' + expect(settings.defaults.get(:new_a, 'zh_CN')).to eq 7 end end @@ -266,7 +115,7 @@ describe SiteSettings::DefaultsProvider do end it 'default_locale always exists' do - expect(settings.defaults.has_setting?(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY)).to be_truthy + expect(settings.defaults.has_setting?(:default_locale)).to be_truthy end it 'returns false when the key is not exist' do diff --git a/spec/components/sql_builder_spec.rb b/spec/components/sql_builder_spec.rb deleted file mode 100644 index 4ccdde49a2..0000000000 --- a/spec/components/sql_builder_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -# encoding: utf-8 -require 'rails_helper' -require_dependency 'sql_builder' - -describe SqlBuilder do - - describe "attached" do - before do - @builder = Post.sql_builder("select * from posts /*where*/ /*limit*/") - end - - it "should find a post by id" do - p = Fabricate(:post) - @builder.where('id = :id and topic_id = :topic_id', id: p.id, topic_id: p.topic_id) - p2 = @builder.exec.first - expect(p2.id).to eq(p.id) - expect(p2).to eq(p) - end - end - - describe "map_exec" do - class SqlBuilder::TestClass - attr_accessor :int, :string, :date, :text, :bool - end - - it "correctly maps to a klass" do - rows = SqlBuilder.new("SELECT - 1 AS int, - 'string' AS string, - CAST(NOW() at time zone 'utc' AS timestamp without time zone) AS date, - 'text'::text AS text, - true AS bool") - .map_exec(SqlBuilder::TestClass) - - expect(rows.count).to eq(1) - row = rows[0] - expect(row.int).to eq(1) - expect(row.string).to eq("string") - expect(row.text).to eq("text") - expect(row.bool).to eq(true) - expect(row.date).to be_within(10.seconds).of(DateTime.now) - end - end - - describe "detached" do - before do - @builder = SqlBuilder.new("select * from (select :a A union all select :b) as X /*where*/ /*order_by*/ /*limit*/ /*offset*/") - end - - it "should allow for 1 param exec" do - expect(@builder.exec(a: 1, b: 2).values[0][0]).to eq(1) - end - - it "should allow for a single where" do - @builder.where(":a = 1") - expect(@builder.exec(a: 1, b: 2).values[0][0]).to eq(1) - end - - it "should allow where chaining" do - @builder.where(":a = 1") - @builder.where("2 = 1") - expect(@builder.exec(a: 1, b: 2).to_a.length).to eq(0) - end - - it "should allow order by" do - expect(@builder.order_by("A desc").limit(1) - .exec(a: 1, b: 2).values[0][0]).to eq(2) - end - it "should allow offset" do - expect(@builder.order_by("A desc").offset(1) - .exec(a: 1, b: 2).values[0][0]).to eq(1) - end - end - -end diff --git a/spec/components/stylesheet/manager_spec.rb b/spec/components/stylesheet/manager_spec.rb index 9e5b6ff347..b534133b9e 100644 --- a/spec/components/stylesheet/manager_spec.rb +++ b/spec/components/stylesheet/manager_spec.rb @@ -87,6 +87,45 @@ describe Stylesheet::Manager do expect(digest1).not_to eq(digest2) end + + let(:image) { file_from_fixtures("logo.png") } + let(:image2) { file_from_fixtures("logo-dev.png") } + + it 'can correctly account for theme uploads in digest' do + theme = Theme.create!( + name: 'parent', + user_id: -1 + ) + + upload = UploadCreator.new(image, "logo.png").create_for(-1) + field = ThemeField.create!( + theme_id: theme.id, + target_id: Theme.targets[:common], + name: "logo", + value: "", + upload_id: upload.id, + type_id: ThemeField.types[:theme_upload_var] + ) + + manager = Stylesheet::Manager.new(:desktop_theme, theme.key) + digest1 = manager.digest + field.destroy! + + upload = UploadCreator.new(image2, "logo.png").create_for(-1) + field = ThemeField.create!( + theme_id: theme.id, + target_id: Theme.targets[:common], + name: "logo", + value: "", + upload_id: upload.id, + type_id: ThemeField.types[:theme_upload_var] + ) + + manager = Stylesheet::Manager.new(:desktop_theme, theme.key) + digest2 = manager.digest + + expect(digest1).not_to eq(digest2) + end end describe 'color_scheme_digest' do diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index fcce481252..08515a3e9d 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -17,8 +17,8 @@ describe TopicQuery do category.set_permissions(group => :full) category.save - topic = Fabricate(:topic, category: category) - topic = Fabricate(:topic, visible: false) + Fabricate(:topic, category: category) + Fabricate(:topic, visible: false) expect(TopicQuery.new(nil).list_latest.topics.count).to eq(0) expect(TopicQuery.new(user).list_latest.topics.count).to eq(0) @@ -98,7 +98,7 @@ describe TopicQuery do context 'bookmarks' do it "filters and returns bookmarks correctly" do post = Fabricate(:post) - reply = Fabricate(:post, topic_id: post.topic_id) + reply = Fabricate(:post, topic: post.topic) post2 = Fabricate(:post) diff --git a/spec/components/topic_view_spec.rb b/spec/components/topic_view_spec.rb index 06fb8099a0..b06b7210fe 100644 --- a/spec/components/topic_view_spec.rb +++ b/spec/components/topic_view_spec.rb @@ -364,6 +364,35 @@ describe TopicView do end end + describe 'when a megalodon topic is closed' do + before do + @original_const = TopicView::MEGA_TOPIC_POSTS_COUNT + TopicView.send(:remove_const, "MEGA_TOPIC_POSTS_COUNT") + TopicView.const_set("MEGA_TOPIC_POSTS_COUNT", 1) + topic.update!(closed: true) + SiteSetting.summary_max_results = 2 + end + + after do + TopicView.send(:remove_const, "MEGA_TOPIC_POSTS_COUNT") + TopicView.const_set("MEGA_TOPIC_POSTS_COUNT", @original_const) + end + + it 'should be forced into summary mode without gaps' do + topic_view = TopicView.new(topic.id, evil_trout, post_number: 1) + + expect(topic_view.contains_gaps?).to eq(false) + expect(topic_view.posts).to eq([p5]) + end + + it 'should not be forced into summary mode if post_number is not blank' do + topic_view = TopicView.new(topic.id, evil_trout, post_number: 2) + + expect(topic_view.contains_gaps?).to eq(false) + expect(topic_view.posts).to eq([p1, p2, p3]) + end + end + it "#restricts to correct topic" do t2 = Fabricate(:topic) @@ -549,4 +578,37 @@ describe TopicView do end end end + + describe '#filtered_post_stream' do + let!(:post) { Fabricate(:post, topic: topic, user: first_poster) } + let!(:post2) { Fabricate(:post, topic: topic, user: evil_trout) } + let!(:post3) { Fabricate(:post, topic: topic, user: first_poster) } + + it 'should return the right columns' do + expect(topic_view.filtered_post_stream).to eq([ + [post.id, post.post_number, 0], + [post2.id, post2.post_number, 0], + [post3.id, post3.post_number, 0] + ]) + end + + describe 'for mega topics' do + it 'should return the right columns' do + begin + original_const = TopicView::MEGA_TOPIC_POSTS_COUNT + TopicView.send(:remove_const, "MEGA_TOPIC_POSTS_COUNT") + TopicView.const_set("MEGA_TOPIC_POSTS_COUNT", 2) + + expect(topic_view.filtered_post_stream).to eq([ + [post.id, post.post_number], + [post2.id, post2.post_number], + [post3.id, post3.post_number] + ]) + ensure + TopicView.send(:remove_const, "MEGA_TOPIC_POSTS_COUNT") + TopicView.const_set("MEGA_TOPIC_POSTS_COUNT", original_const) + end + end + end + end end diff --git a/spec/components/validators/upload_validator_spec.rb b/spec/components/validators/upload_validator_spec.rb index 77dd4a1f0c..164d0b0a8e 100644 --- a/spec/components/validators/upload_validator_spec.rb +++ b/spec/components/validators/upload_validator_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require_dependency 'validators/upload_validator' describe Validators::UploadValidator do subject(:validator) { described_class.new } diff --git a/spec/components/wizard/step_updater_spec.rb b/spec/components/wizard/step_updater_spec.rb index ddc3513508..b57d3000db 100644 --- a/spec/components/wizard/step_updater_spec.rb +++ b/spec/components/wizard/step_updater_spec.rb @@ -237,6 +237,15 @@ describe Wizard::StepUpdater do expect(SiteSetting.favicon_url).to eq('/uploads/favicon.png') expect(SiteSetting.apple_touch_icon_url).to eq('/uploads/apple.png') end + + it "updates large_icon_url if the uploaded icon size is greater than 180x180" do + upload = Fabricate(:upload, width: 512, height: 512) + updater = wizard.create_updater('icons', apple_touch_icon_url: upload.url) + updater.update + + expect(updater).to be_success + expect(SiteSetting.large_icon_url).to eq(upload.url) + end end context "emoji step" do @@ -252,12 +261,13 @@ describe Wizard::StepUpdater do context "homepage step" do it "updates the fields correctly" do - updater = wizard.create_updater('homepage', homepage_style: "categories") + updater = wizard.create_updater('homepage', homepage_style: "categories_and_top_topics") updater.update expect(updater).to be_success expect(wizard.completed_steps?('homepage')).to eq(true) expect(SiteSetting.top_menu).to eq('categories|latest|new|unread|top') + expect(SiteSetting.desktop_category_page_style).to eq('categories_and_top_topics') updater = wizard.create_updater('homepage', homepage_style: "latest") updater.update diff --git a/spec/controllers/admin/admin_controller_spec.rb b/spec/controllers/admin/admin_controller_spec.rb deleted file mode 100644 index f26b081a9d..0000000000 --- a/spec/controllers/admin/admin_controller_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'rails_helper' - -describe Admin::AdminController do - - context 'index' do - - it 'needs you to be logged in' do - get :index, format: :json - expect(response.status).to eq(403) - end - - it "raises an error if you aren't an admin" do - _user = log_in - get :index, format: :json - expect(response).to be_forbidden - end - - end - -end diff --git a/spec/controllers/admin/api_controller_spec.rb b/spec/controllers/admin/api_controller_spec.rb deleted file mode 100644 index af72b0529a..0000000000 --- a/spec/controllers/admin/api_controller_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -require 'rails_helper' - -describe Admin::ApiController do - - it "is a subclass of AdminController" do - expect(Admin::ApiController < Admin::AdminController).to eq(true) - end - - let!(:user) { log_in(:admin) } - - context '.index' do - it "succeeds" do - get :index, format: :json - expect(response).to be_success - end - end - - context '.regenerate_key' do - let(:api_key) { Fabricate(:api_key) } - - it "returns 404 when there is no key" do - put :regenerate_key, params: { id: 1234 }, format: :json - expect(response).not_to be_success - expect(response.status).to eq(404) - end - - it "delegates to the api key's `regenerate!` method" do - ApiKey.any_instance.expects(:regenerate!) - put :regenerate_key, params: { id: api_key.id }, format: :json - end - end - - context '.revoke_key' do - let(:api_key) { Fabricate(:api_key) } - - it "returns 404 when there is no key" do - delete :revoke_key, params: { id: 1234 }, format: :json - expect(response).not_to be_success - expect(response.status).to eq(404) - end - - it "delegates to the api key's `regenerate!` method" do - ApiKey.any_instance.expects(:destroy) - delete :revoke_key, params: { id: api_key.id }, format: :json - end - end - - context '.create_master_key' do - it "creates a record" do - expect do - post :create_master_key, format: :json - end.to change(ApiKey, :count).by(1) - end - end - -end diff --git a/spec/controllers/admin/backups_controller_spec.rb b/spec/controllers/admin/backups_controller_spec.rb deleted file mode 100644 index 4f7a6c7894..0000000000 --- a/spec/controllers/admin/backups_controller_spec.rb +++ /dev/null @@ -1,264 +0,0 @@ -require "rails_helper" - -describe Admin::BackupsController do - - it "is a subclass of AdminController" do - expect(Admin::BackupsController < Admin::AdminController).to eq(true) - end - - let(:backup_filename) { "2014-02-10-065935.tar.gz" } - - context "while logged in as an admin" do - - before { @admin = log_in(:admin) } - - describe ".index" do - - context "html format" do - - it "preloads important data" do - Backup.expects(:all).returns([]) - subject.expects(:store_preloaded).with("backups", "[]") - - BackupRestore.expects(:operations_status).returns({}) - subject.expects(:store_preloaded).with("operations_status", "{}") - - BackupRestore.expects(:logs).returns([]) - subject.expects(:store_preloaded).with("logs", "[]") - - get :index, format: :html, xhr: true - - expect(response).to be_success - end - - end - - context "json format" do - - it "returns a list of all the backups" do - Backup.expects(:all).returns([Backup.new("backup1"), Backup.new("backup2")]) - - get :index, format: :json, xhr: true - - expect(response).to be_success - - json = JSON.parse(response.body) - expect(json[0]["filename"]).to eq("backup1") - expect(json[1]["filename"]).to eq("backup2") - end - - end - - end - - describe ".status" do - - it "returns the current backups status" do - BackupRestore.expects(:operations_status) - - get :status, format: :json - - expect(response).to be_success - end - - end - - describe ".create" do - - it "starts a backup" do - BackupRestore.expects(:backup!).with(@admin.id, publish_to_message_bus: true, with_uploads: false, client_id: "foo") - - post :create, params: { - with_uploads: false, client_id: "foo" - }, format: :json - - expect(response).to be_success - end - - end - - describe ".show" do - - it "uses send_file to transmit the backup" do - begin - token = EmailBackupToken.set(@admin.id) - path = File.join(Backup.base_directory, backup_filename) - File.open(path, "w") { |f| f.write("hello") } - - Backup.create_from_filename(backup_filename) - - StaffActionLogger.any_instance.expects(:log_backup_download).once - - get :show, params: { id: backup_filename, token: token }, format: :json - - expect(response.headers['Content-Length']).to eq("5") - expect(response.headers['Content-Disposition']).to match(/attachment; filename/) - ensure - File.delete(path) - EmailBackupToken.del(@admin.id) - end - end - - it "returns 422 when token is bad" do - begin - path = File.join(Backup.base_directory, backup_filename) - File.open(path, "w") { |f| f.write("hello") } - - Backup.create_from_filename(backup_filename) - - get :show, params: { id: backup_filename, token: "bad_value" }, xhr: true - - expect(response.status).to eq(422) - ensure - File.delete(path) - end - end - - it "returns 404 when the backup does not exist" do - token = EmailBackupToken.set(@admin.id) - Backup.expects(:[]).returns(nil) - - get :show, params: { id: backup_filename, token: token }, format: :json - - EmailBackupToken.del(@admin.id) - - expect(response).to be_not_found - end - - end - - describe ".destroy" do - - let(:b) { Backup.new(backup_filename) } - - it "removes the backup if found" do - Backup.expects(:[]).with(backup_filename).returns(b) - b.expects(:remove) - - StaffActionLogger.any_instance.expects(:log_backup_destroy).with(b).once - - delete :destroy, params: { id: backup_filename }, format: :json - - expect(response).to be_success - end - - it "doesn't remove the backup if not found" do - Backup.expects(:[]).with(backup_filename).returns(nil) - b.expects(:remove).never - delete :destroy, params: { id: backup_filename }, format: :json - expect(response).not_to be_success - end - - end - - describe ".logs" do - - it "preloads important data" do - BackupRestore.expects(:operations_status).returns({}) - subject.expects(:store_preloaded).with("operations_status", "{}") - - BackupRestore.expects(:logs).returns([]) - subject.expects(:store_preloaded).with("logs", "[]") - - get :logs, format: :html, xhr: true - - expect(response).to be_success - end - end - - describe ".restore" do - - it "starts a restore" do - expect(SiteSetting.disable_emails).to eq(false) - BackupRestore.expects(:restore!).with(@admin.id, filename: backup_filename, publish_to_message_bus: true, client_id: "foo") - - post :restore, params: { id: backup_filename, client_id: "foo" }, format: :json - - expect(SiteSetting.disable_emails).to eq(true) - expect(response).to be_success - end - - end - - describe ".readonly" do - - it "enables readonly mode" do - Discourse.expects(:enable_readonly_mode) - - expect { put :readonly, params: { enable: true }, format: :json } - .to change { UserHistory.count }.by(1) - - expect(response).to be_success - - user_history = UserHistory.last - - expect(UserHistory.last.action).to eq(UserHistory.actions[:change_readonly_mode]) - expect(UserHistory.last.new_value).to eq('t') - end - - it "disables readonly mode" do - Discourse.expects(:disable_readonly_mode) - - expect { put :readonly, params: { enable: false }, format: :json } - .to change { UserHistory.count }.by(1) - - expect(response).to be_success - - user_history = UserHistory.last - - expect(UserHistory.last.action).to eq(UserHistory.actions[:change_readonly_mode]) - expect(UserHistory.last.new_value).to eq('f') - end - - end - - describe "#upload_backup_chunk" do - describe "when filename contains invalid characters" do - it "should raise an error" do - ['灰色.tar.gz', '; echo \'haha\'.tar.gz'].each do |invalid_filename| - described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) - - post :upload_backup_chunk, params: { - resumableFilename: invalid_filename, resumableTotalSize: 1 - } - - expect(response.status).to eq(415) - expect(response.body).to eq(I18n.t('backup.invalid_filename')) - end - end - end - - describe "when filename is valid" do - it "should upload the file successfully" do - begin - described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) - - filename = 'test_Site-0123456789.tar.gz' - - post :upload_backup_chunk, params: { - resumableFilename: filename, - resumableTotalSize: 1, - resumableIdentifier: 'test', - resumableChunkNumber: '1', - resumableChunkSize: '1', - resumableCurrentChunkSize: '1', - file: fixture_file_upload(Tempfile.new) - }, format: :json - - expect(response.status).to eq(200) - expect(response.body).to eq("") - ensure - begin - File.delete( - File.join(Backup.base_directory, 'tmp', 'test', "#{filename}.part1") - ) - rescue Errno::ENOENT - end - end - end - end - end - - end - -end diff --git a/spec/controllers/admin/emojis_controller_spec.rb b/spec/controllers/admin/emojis_controller_spec.rb deleted file mode 100644 index 1b494f0571..0000000000 --- a/spec/controllers/admin/emojis_controller_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -require "rails_helper" - -describe Admin::EmojisController do - - let(:custom_emoji) do - Emoji.new("/path/to/hello").tap do |e| - e.name = "hello" - e.url = "/url/to/hello.png" - end - end - - let(:custom_emoji2) do - Emoji.new("/path/to/hello2").tap do |e| - e.name = "hello2" - e.url = "/url/to/hello2.png" - end - end - - context "when logged in" do - let!(:user) { log_in(:admin) } - - context ".index" do - it "returns a list of custom emojis" do - Emoji.expects(:custom).returns([custom_emoji]) - get :index, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json[0]["name"]).to eq(custom_emoji.name) - expect(json[0]["url"]).to eq(custom_emoji.url) - end - end - end - -end diff --git a/spec/controllers/admin/impersonate_controller_spec.rb b/spec/controllers/admin/impersonate_controller_spec.rb deleted file mode 100644 index 866e6898b5..0000000000 --- a/spec/controllers/admin/impersonate_controller_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require 'rails_helper' - -describe Admin::ImpersonateController do - - it "is a subclass of AdminController" do - expect(Admin::ImpersonateController < Admin::AdminController).to eq(true) - end - - context 'while logged in as an admin' do - let!(:admin) { log_in(:admin) } - let(:user) { Fabricate(:user) } - - context 'index' do - it 'returns success' do - get :index, format: :json - expect(response).to be_success - end - end - - context 'create' do - - it 'requires a username_or_email parameter' do - expect { put :create, format: :json }.to raise_error(ActionController::ParameterMissing) - end - - it 'returns 404 when that user does not exist' do - post :create, params: { username_or_email: 'hedonismbot' }, format: :json - expect(response.status).to eq(404) - end - - it "raises an invalid access error if the user can't be impersonated" do - Guardian.any_instance.expects(:can_impersonate?).with(user).returns(false) - post :create, params: { username_or_email: user.email }, format: :json - expect(response).to be_forbidden - end - - context 'success' do - - it "logs the impersonation" do - StaffActionLogger.any_instance.expects(:log_impersonate) - post :create, params: { username_or_email: user.username }, format: :json - end - - it "changes the current user session id" do - post :create, params: { username_or_email: user.username }, format: :json - expect(session[:current_user_id]).to eq(user.id) - end - - it "returns success" do - post :create, params: { username_or_email: user.email }, format: :json - expect(response).to be_success - end - - it "also works with an email address" do - post :create, params: { username_or_email: user.email }, format: :json - expect(session[:current_user_id]).to eq(user.id) - end - - end - - end - - end - -end diff --git a/spec/controllers/admin/site_settings_controller_spec.rb b/spec/controllers/admin/site_settings_controller_spec.rb deleted file mode 100644 index 874f375602..0000000000 --- a/spec/controllers/admin/site_settings_controller_spec.rb +++ /dev/null @@ -1,81 +0,0 @@ -require 'rails_helper' - -describe Admin::SiteSettingsController do - - it "is a subclass of AdminController" do - expect(Admin::SiteSettingsController < Admin::AdminController).to eq(true) - end - - context 'while logged in as an admin' do - before do - @user = log_in(:admin) - end - - context 'index' do - it 'returns success' do - get :index, format: :json - expect(response).to be_success - end - - it 'returns JSON' do - get :index, format: :json - expect(::JSON.parse(response.body)).to be_present - end - end - - context 'update' do - - before do - SiteSetting.setting(:test_setting, "default") - SiteSetting.refresh! - end - - it 'sets the value when the param is present' do - put :update, params: { - id: 'test_setting', test_setting: 'hello' - }, format: :json - - expect(SiteSetting.test_setting).to eq('hello') - end - - it 'allows value to be a blank string' do - put :update, params: { - id: 'test_setting', test_setting: '' - }, format: :json - - expect(SiteSetting.test_setting).to eq('') - end - - it 'logs the change' do - SiteSetting.test_setting = 'previous' - StaffActionLogger.any_instance.expects(:log_site_setting_change).with('test_setting', 'previous', 'hello') - - put :update, params: { - id: 'test_setting', test_setting: 'hello' - }, format: :json - - expect(SiteSetting.test_setting).to eq('hello') - end - - it 'does not allow changing of hidden settings' do - SiteSetting.setting(:hidden_setting, "hidden", hidden: true) - SiteSetting.refresh! - - put :update, params: { - id: 'hidden_setting', hidden_setting: 'not allowed' - }, format: :json - - expect(SiteSetting.hidden_setting).to eq("hidden") - expect(response.status).to eq(422) - end - - it 'fails when a setting does not exist' do - expect { - put :update, params: { id: 'provider', provider: 'gotcha' }, format: :json - }.to raise_error(ArgumentError) - end - end - - end - -end diff --git a/spec/controllers/admin/site_texts_controller_spec.rb b/spec/controllers/admin/site_texts_controller_spec.rb deleted file mode 100644 index 7a1676a831..0000000000 --- a/spec/controllers/admin/site_texts_controller_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -require 'rails_helper' - -describe Admin::SiteTextsController do - - it "is a subclass of AdminController" do - expect(Admin::SiteTextsController < Admin::AdminController).to eq(true) - end - - context 'while logged in as an admin' do - before do - @user = log_in(:admin) - end - - context '.index' do - it 'returns json' do - get :index, params: { q: 'title' }, format: :json - expect(response).to be_success - expect(::JSON.parse(response.body)).to be_present - end - end - - context '.show' do - it 'returns a site text for a key that exists' do - get :show, params: { id: 'title' }, format: :json - expect(response).to be_success - - json = ::JSON.parse(response.body) - expect(json).to be_present - - site_text = json['site_text'] - expect(site_text).to be_present - - expect(site_text['id']).to eq('title') - expect(site_text['value']).to eq(I18n.t(:title)) - end - - it 'returns not found for missing keys' do - get :show, params: { id: 'made_up_no_key_exists' }, format: :json - expect(response).not_to be_success - end - end - - context '#update and #revert' do - after do - TranslationOverride.delete_all - I18n.reload! - end - - describe 'failure' do - before do - I18n.backend.store_translations(:en, some_key: '%{first} %{second}') - end - - it 'returns the right error message' do - put :update, params: { - id: 'some_key', site_text: { value: 'hello %{key} %{omg}' } - }, format: :json - - expect(response.status).to eq(422) - - body = JSON.parse(response.body) - - expect(body['message']).to eq(I18n.t( - 'activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys', - keys: 'key, omg' - )) - end - end - - it 'updates and reverts the key' do - orig_title = I18n.t(:title) - - put :update, params: { id: 'title', site_text: { value: 'hello' } }, format: :json - expect(response).to be_success - - json = ::JSON.parse(response.body) - expect(json).to be_present - - site_text = json['site_text'] - expect(site_text).to be_present - - expect(site_text['id']).to eq('title') - expect(site_text['value']).to eq('hello') - - # Revert - put :revert, params: { id: 'title' }, format: :json - expect(response).to be_success - - json = ::JSON.parse(response.body) - expect(json).to be_present - - site_text = json['site_text'] - expect(site_text).to be_present - - expect(site_text['id']).to eq('title') - expect(site_text['value']).to eq(orig_title) - end - - it 'returns not found for missing keys' do - put :update, params: { - id: 'made_up_no_key_exists', site_text: { value: 'hello' } - }, format: :json - - expect(response).not_to be_success - end - - it 'logs the change' do - original_title = I18n.t(:title) - - put :update, params: { - id: 'title', site_text: { value: 'yay' } - }, format: :json - - log = UserHistory.last - - expect(log.previous_value).to eq(original_title) - expect(log.new_value).to eq('yay') - expect(log.action).to eq(UserHistory.actions[:change_site_text]) - - put :revert, params: { id: 'title' }, format: :json - - log = UserHistory.last - - expect(log.previous_value).to eq('yay') - expect(log.new_value).to eq(original_title) - expect(log.action).to eq(UserHistory.actions[:change_site_text]) - end - - it 'returns site texts for the correct locale' do - SiteSetting.default_locale = :ru - - ru_title = 'title ru' - ru_mf_text = 'ru {NUM_RESULTS, plural, one {1 result} other {many} }' - - put :update, params: { id: 'title', site_text: { value: ru_title } }, format: :json - put :update, params: { id: 'js.topic.read_more_MF', site_text: { value: ru_mf_text } }, format: :json - - get :show, params: { id: 'title' }, format: :json - json = ::JSON.parse(response.body) - expect(json['site_text']['value']).to eq(ru_title) - - get :show, params: { id: 'js.topic.read_more_MF' }, format: :json - json = ::JSON.parse(response.body) - expect(json['site_text']['value']).to eq(ru_mf_text) - - SiteSetting.default_locale = :en - - get :show, params: { id: 'title' }, format: :json - json = ::JSON.parse(response.body) - expect(json['site_text']['value']).to_not eq(ru_title) - - get :show, params: { id: 'js.topic.read_more_MF' }, format: :json - json = ::JSON.parse(response.body) - expect(json['site_text']['value']).to_not eq(ru_mf_text) - end - end - end - -end diff --git a/spec/controllers/admin/themes_controller_spec.rb b/spec/controllers/admin/themes_controller_spec.rb deleted file mode 100644 index c2c00b78df..0000000000 --- a/spec/controllers/admin/themes_controller_spec.rb +++ /dev/null @@ -1,205 +0,0 @@ -require 'rails_helper' -require_dependency 'theme_serializer' - -describe Admin::ThemesController do - - it "is a subclass of AdminController" do - expect(Admin::UsersController < Admin::AdminController).to eq(true) - end - - context 'while logged in as an admin' do - before do - @user = log_in(:admin) - end - - context '.generate_key_pair' do - it 'can generate key pairs' do - post :generate_key_pair, format: :json - json = JSON.parse(response.body) - expect(json["private_key"]).to include("RSA PRIVATE KEY") - expect(json["public_key"]).to include("ssh-rsa ") - end - end - - context '.upload_asset' do - render_views - - let(:upload) do - Rack::Test::UploadedFile.new(file_from_fixtures("fake.woff2", "woff2")) - end - - it 'can create a theme upload' do - post :upload_asset, params: { file: upload }, format: :json - expect(response.status).to eq(201) - - upload = Upload.find_by(original_filename: "fake.woff2") - - expect(upload.id).not_to be_nil - expect(JSON.parse(response.body)["upload_id"]).to eq(upload.id) - end - end - - context '.import' do - let(:theme_file) do - Rack::Test::UploadedFile.new(file_from_fixtures("sam-s-simple-theme.dcstyle.json", "json")) - end - - let :image do - file_from_fixtures("logo.png") - end - - it 'can import a theme with an upload' do - upload = Fabricate(:upload) - theme = Theme.new(name: 'with-upload', user_id: -1) - upload = UploadCreator.new(image, "logo.png").create_for(-1) - theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var) - theme.save - - json = ThemeWithEmbeddedUploadsSerializer.new(theme, root: 'theme').to_json - theme.destroy - - temp = Tempfile.new - temp.write(json) - temp.rewind - - uploaded_json = Rack::Test::UploadedFile.new(temp) - upload.destroy - - post :import, params: { theme: uploaded_json }, format: :json - expect(response).to be_success - temp.unlink - - theme = Theme.last - expect(theme.theme_fields.count).to eq(1) - expect(theme.theme_fields.first.upload).not_to eq(nil) - expect(theme.theme_fields.first.upload.filesize).to eq(upload.filesize) - expect(theme.theme_fields.first.upload.sha1).to eq(upload.sha1) - expect(theme.theme_fields.first.upload.original_filename).to eq(upload.original_filename) - end - - it 'imports a theme' do - post :import, params: { theme: theme_file }, format: :json - expect(response).to be_success - - json = ::JSON.parse(response.body) - - expect(json["theme"]["name"]).to eq("Sam's Simple Theme") - expect(json["theme"]["theme_fields"].length).to eq(2) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) - end - end - - context ' .index' do - it 'correctly returns themes' do - - ColorScheme.destroy_all - Theme.destroy_all - - theme = Theme.new(name: 'my name', user_id: -1) - theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') - theme.set_field(target: :desktop, name: :after_header, value: 'test') - - theme.remote_theme = RemoteTheme.new( - remote_url: 'awesome.git', - remote_version: '7', - local_version: '8', - remote_updated_at: Time.zone.now - ) - - theme.save! - - # this will get serialized as well - ColorScheme.create_from_base(name: "test", colors: []) - - get :index, format: :json - - expect(response).to be_success - - json = ::JSON.parse(response.body) - - expect(json["extras"]["color_schemes"].length).to eq(2) - theme_json = json["themes"].find { |t| t["id"] == theme.id } - expect(theme_json["theme_fields"].length).to eq(2) - expect(theme_json["remote_theme"]["remote_version"]).to eq("7") - end - end - - context ' .create' do - it 'creates a theme' do - post :create, params: { - theme: { - name: 'my test name', - theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}'] - } - }, format: :json - - expect(response).to be_success - - json = ::JSON.parse(response.body) - - expect(json["theme"]["theme_fields"].length).to eq(1) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) - end - end - - context ' .update' do - let(:theme) { Theme.create(name: 'my name', user_id: -1) } - - it 'can change default theme' do - SiteSetting.default_theme_key = nil - - put :update, params: { - id: theme.id, theme: { default: true } - }, format: :json - - expect(SiteSetting.default_theme_key).to eq(theme.key) - end - - it 'can unset default theme' do - SiteSetting.default_theme_key = theme.key - - put :update, params: { - id: theme.id, theme: { default: false } - }, format: :json - - expect(SiteSetting.default_theme_key).to be_blank - end - - it 'updates a theme' do - theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') - theme.save - - child_theme = Theme.create(name: 'my name', user_id: -1) - - upload = Fabricate(:upload) - - put :update, params: { - id: theme.id, - theme: { - child_theme_ids: [child_theme.id], - name: 'my test name', - theme_fields: [ - { name: 'scss', target: 'common', value: '' }, - { name: 'scss', target: 'desktop', value: 'body{color: blue;}' }, - { name: 'bob', target: 'common', value: '', type_id: 2, upload_id: upload.id }, - ] - } - }, format: :json - - expect(response).to be_success - - json = ::JSON.parse(response.body) - - fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } - - expect(fields[0]["value"]).to eq('') - expect(fields[0]["upload_id"]).to eq(upload.id) - expect(fields[1]["value"]).to eq('body{color: blue;}') - expect(fields.length).to eq(2) - expect(json["theme"]["child_themes"].length).to eq(1) - expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) - end - end - end - -end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb deleted file mode 100644 index d026d7bfb5..0000000000 --- a/spec/controllers/admin/users_controller_spec.rb +++ /dev/null @@ -1,840 +0,0 @@ -require 'rails_helper' -require_dependency 'single_sign_on' - -describe Admin::UsersController do - - it 'is a subclass of AdminController' do - expect(Admin::UsersController < Admin::AdminController).to eq(true) - end - - context 'while logged in as an admin' do - before do - @user = log_in(:admin) - end - - context '#index' do - it 'returns success' do - get :index, format: :json - expect(response).to be_success - end - - it 'returns JSON' do - get :index, format: :json - expect(::JSON.parse(response.body)).to be_present - end - - context 'when showing emails' do - - it "returns email for all the users" do - get :index, params: { show_emails: "true" }, format: :json - data = ::JSON.parse(response.body) - data.each do |user| - expect(user["email"]).to be_present - end - end - - it "logs only 1 enty" do - expect(UserHistory.where(action: UserHistory.actions[:check_email], acting_user_id: @user.id).count).to eq(0) - - get :index, params: { show_emails: "true" }, format: :json - - expect(UserHistory.where(action: UserHistory.actions[:check_email], acting_user_id: @user.id).count).to eq(1) - end - - end - end - - describe '#show' do - context 'an existing user' do - it 'returns success' do - get :show, params: { id: @user.id }, format: :json - expect(response).to be_success - end - end - - context 'an existing user' do - it 'returns success' do - get :show, params: { id: 0 }, format: :json - expect(response).not_to be_success - end - end - end - - context '#approve_bulk' do - - let(:evil_trout) { Fabricate(:evil_trout) } - - it "does nothing without uesrs" do - User.any_instance.expects(:approve).never - put :approve_bulk, format: :json - end - - it "won't approve the user when not allowed" do - Guardian.any_instance.expects(:can_approve?).with(evil_trout).returns(false) - User.any_instance.expects(:approve).never - put :approve_bulk, params: { users: [evil_trout.id] }, format: :json - end - - it "approves the user when permitted" do - Guardian.any_instance.expects(:can_approve?).with(evil_trout).returns(true) - User.any_instance.expects(:approve).once - put :approve_bulk, params: { users: [evil_trout.id] }, format: :json - end - - end - - context '#generate_api_key' do - let(:evil_trout) { Fabricate(:evil_trout) } - - it 'calls generate_api_key' do - User.any_instance.expects(:generate_api_key).with(@user) - post :generate_api_key, params: { user_id: evil_trout.id }, format: :json - end - end - - context '#revoke_api_key' do - - let(:evil_trout) { Fabricate(:evil_trout) } - - it 'calls revoke_api_key' do - User.any_instance.expects(:revoke_api_key) - delete :revoke_api_key, params: { user_id: evil_trout.id }, format: :json - end - - end - - context '#approve' do - - let(:evil_trout) { Fabricate(:evil_trout) } - - it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_approve?).with(evil_trout).returns(false) - put :approve, params: { user_id: evil_trout.id }, format: :json - expect(response).to be_forbidden - end - - it 'calls approve' do - User.any_instance.expects(:approve).with(@user) - put :approve, params: { user_id: evil_trout.id }, format: :json - end - - end - - context '#suspend' do - let(:user) { Fabricate(:evil_trout) } - - it "works properly" do - Fabricate(:api_key, user: user) - expect(user).not_to be_suspended - put( - :suspend, - params: { - user_id: user.id, - suspend_until: 5.hours.from_now, - reason: "because I said so", - format: :json - } - ) - expect(response).to be_success - - user.reload - expect(user).to be_suspended - expect(user.suspended_at).to be_present - expect(user.suspended_till).to be_present - expect(ApiKey.where(user_id: user.id).count).to eq(0) - - log = UserHistory.where(target_user_id: user.id).order('id desc').first - expect(log).to be_present - expect(log.details).to match(/because I said so/) - end - - context "with an associated post" do - let(:post) { Fabricate(:post) } - let(:suspend_params) do - { user_id: user.id, - suspend_until: 5.hours.from_now, - reason: "because of this post", - post_id: post.id, - format: :json } - end - - it "can have an associated post" do - put(:suspend, params: suspend_params) - expect(response).to be_success - - log = UserHistory.where(target_user_id: user.id).order('id desc').first - expect(log).to be_present - expect(log.post_id).to eq(post.id) - end - - it "can delete an associated post" do - put(:suspend, params: suspend_params.merge(post_action: 'delete')) - post.reload - expect(post.deleted_at).to be_present - expect(response).to be_success - end - - it "can edit an associated post" do - put(:suspend, params: suspend_params.merge( - post_action: 'edit', - post_edit: 'this is the edited content' - )) - post.reload - expect(post.deleted_at).to be_blank - expect(post.raw).to eq("this is the edited content") - expect(response).to be_success - end - end - - it "can send a message to the user" do - Jobs.expects(:enqueue).with( - :critical_user_email, - has_entries( - type: :account_suspended, - user_id: user.id - ) - ) - - put( - :suspend, - params: { - user_id: user.id, - suspend_until: 10.days.from_now, - reason: "short reason", - message: "long reason", - format: :json - } - ) - expect(response).to be_success - - log = UserHistory.where(target_user_id: user.id).order('id desc').first - expect(log).to be_present - expect(log.details).to match(/short reason/) - expect(log.details).to match(/long reason/) - end - - it "also revoke any api keys" do - User.any_instance.expects(:revoke_api_key) - put :suspend, params: { user_id: user.id }, format: :json - end - - end - - context '#revoke_admin' do - before do - @another_admin = Fabricate(:admin) - end - - it 'raises an error unless the user can revoke access' do - Guardian.any_instance.expects(:can_revoke_admin?).with(@another_admin).returns(false) - put :revoke_admin, params: { user_id: @another_admin.id }, format: :json - expect(response).to be_forbidden - end - - it 'updates the admin flag' do - put :revoke_admin, params: { user_id: @another_admin.id }, format: :json - @another_admin.reload - expect(@another_admin).not_to be_admin - end - end - - context '#grant_admin' do - before do - @another_user = Fabricate(:coding_horror) - end - - after do - $redis.flushall - end - - it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_grant_admin?).with(@another_user).returns(false) - put :grant_admin, params: { user_id: @another_user.id }, format: :json - expect(response).to be_forbidden - end - - it "returns a 404 if the username doesn't exist" do - put :grant_admin, params: { user_id: 123123 }, format: :json - expect(response).to be_forbidden - end - - it 'updates the admin flag' do - expect(AdminConfirmation.exists_for?(@another_user.id)).to eq(false) - put :grant_admin, params: { user_id: @another_user.id }, format: :json - expect(AdminConfirmation.exists_for?(@another_user.id)).to eq(true) - end - end - - context '#add_group' do - let(:user) { Fabricate(:user) } - let(:group) { Fabricate(:group) } - - it 'adds the user to the group' do - post :add_group, params: { - group_id: group.id, user_id: user.id - }, format: :json - - expect(response).to be_success - expect(GroupUser.where(user_id: user.id, group_id: group.id).exists?).to eq(true) - - group_history = GroupHistory.last - - expect(group_history.action).to eq(GroupHistory.actions[:add_user_to_group]) - expect(group_history.acting_user).to eq(@user) - expect(group_history.target_user).to eq(user) - - # Doing it again doesn't raise an error - post :add_group, params: { - group_id: group.id, user_id: user.id - }, format: :json - - expect(response).to be_success - end - end - - context '#primary_group' do - let(:group) { Fabricate(:group) } - - before do - @another_user = Fabricate(:coding_horror) - end - - it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_change_primary_group?).with(@another_user).returns(false) - put :primary_group, params: { - user_id: @another_user.id - }, format: :json - - expect(response).to be_forbidden - end - - it "returns a 404 if the user doesn't exist" do - put :primary_group, params: { - user_id: 123123 - }, format: :json - - expect(response).to be_forbidden - end - - it "changes the user's primary group" do - group.add(@another_user) - put :primary_group, params: { - user_id: @another_user.id, primary_group_id: group.id - }, format: :json - - @another_user.reload - expect(@another_user.primary_group_id).to eq(group.id) - end - - it "doesn't change primary group if they aren't a member of the group" do - put :primary_group, params: { - user_id: @another_user.id, primary_group_id: group.id - }, format: :json - - @another_user.reload - expect(@another_user.primary_group_id).to be_nil - end - - it "remove user's primary group" do - group.add(@another_user) - - put :primary_group, params: { - user_id: @another_user.id, primary_group_id: "" - }, format: :json - - @another_user.reload - expect(@another_user.primary_group_id).to be(nil) - end - end - - context '#trust_level' do - before do - @another_user = Fabricate(:coding_horror, created_at: 1.month.ago) - end - - it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_change_trust_level?).with(@another_user).returns(false) - put :trust_level, params: { - user_id: @another_user.id - }, format: :json - - expect(response).not_to be_success - end - - it "returns a 404 if the username doesn't exist" do - put :trust_level, params: { - user_id: 123123 - }, format: :json - - expect(response).not_to be_success - end - - it "upgrades the user's trust level" do - StaffActionLogger.any_instance.expects(:log_trust_level_change).with(@another_user, @another_user.trust_level, 2).once - - put :trust_level, params: { - user_id: @another_user.id, level: 2 - }, format: :json - - @another_user.reload - expect(@another_user.trust_level).to eq(2) - expect(response).to be_success - end - - it "raises no error when demoting a user below their current trust level (locks trust level)" do - stat = @another_user.user_stat - stat.topics_entered = SiteSetting.tl1_requires_topics_entered + 1 - stat.posts_read_count = SiteSetting.tl1_requires_read_posts + 1 - stat.time_read = SiteSetting.tl1_requires_time_spent_mins * 60 - stat.save! - @another_user.update_attributes(trust_level: TrustLevel[1]) - - put :trust_level, params: { - user_id: @another_user.id, - level: TrustLevel[0] - }, format: :json - - expect(response).to be_success - @another_user.reload - expect(@another_user.trust_level).to eq(TrustLevel[0]) - expect(@another_user.manual_locked_trust_level).to eq(TrustLevel[0]) - end - end - - describe '#revoke_moderation' do - before do - @moderator = Fabricate(:moderator) - end - - it 'raises an error unless the user can revoke access' do - Guardian.any_instance.expects(:can_revoke_moderation?).with(@moderator).returns(false) - put :revoke_moderation, params: { - user_id: @moderator.id - }, format: :json - - expect(response).to be_forbidden - end - - it 'updates the moderator flag' do - put :revoke_moderation, params: { - user_id: @moderator.id - }, format: :json - - @moderator.reload - expect(@moderator.moderator).not_to eq(true) - end - end - - context '#grant_moderation' do - before do - @another_user = Fabricate(:coding_horror) - end - - it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_grant_moderation?).with(@another_user).returns(false) - put :grant_moderation, params: { user_id: @another_user.id }, format: :json - expect(response).to be_forbidden - end - - it "returns a 404 if the username doesn't exist" do - put :grant_moderation, params: { user_id: 123123 }, format: :json - expect(response).to be_forbidden - end - - it 'updates the moderator flag' do - put :grant_moderation, params: { user_id: @another_user.id }, format: :json - @another_user.reload - expect(@another_user.moderator).to eq(true) - end - end - - context '#reject_bulk' do - let(:reject_me) { Fabricate(:user) } - let(:reject_me_too) { Fabricate(:user) } - - it 'does nothing without users' do - UserDestroyer.any_instance.expects(:destroy).never - delete :reject_bulk, format: :json - end - - it "won't delete users if not allowed" do - Guardian.any_instance.stubs(:can_delete_user?).returns(false) - UserDestroyer.any_instance.expects(:destroy).never - - delete :reject_bulk, params: { - users: [reject_me.id] - }, format: :json - end - - it "reports successes" do - Guardian.any_instance.stubs(:can_delete_user?).returns(true) - UserDestroyer.any_instance.stubs(:destroy).returns(true) - - delete :reject_bulk, params: { - users: [reject_me.id, reject_me_too.id] - }, format: :json - - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json['success'].to_i).to eq(2) - expect(json['failed'].to_i).to eq(0) - end - - context 'failures' do - before do - Guardian.any_instance.stubs(:can_delete_user?).returns(true) - end - - it 'can handle some successes and some failures' do - UserDestroyer.any_instance.stubs(:destroy).with(reject_me, anything).returns(false) - UserDestroyer.any_instance.stubs(:destroy).with(reject_me_too, anything).returns(true) - - delete :reject_bulk, params: { - users: [reject_me.id, reject_me_too.id] - }, format: :json - - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json['success'].to_i).to eq(1) - expect(json['failed'].to_i).to eq(1) - end - - it 'reports failure due to a user still having posts' do - UserDestroyer.any_instance.expects(:destroy).with(reject_me, anything).raises(UserDestroyer::PostsExistError) - - delete :reject_bulk, params: { - users: [reject_me.id] - }, format: :json - - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json['success'].to_i).to eq(0) - expect(json['failed'].to_i).to eq(1) - end - end - end - - context '#destroy' do - let(:delete_me) { Fabricate(:user) } - - it "returns a 403 if the user doesn't exist" do - delete :destroy, params: { id: 123123 }, format: :json - expect(response).to be_forbidden - end - - context "user has post" do - let(:topic) { create_topic(user: delete_me) } - - before do - _post = create_post(topic: topic, user: delete_me) - end - - it "returns an api response that the user can't be deleted because it has posts" do - delete :destroy, params: { id: delete_me.id }, format: :json - expect(response).to be_forbidden - json = ::JSON.parse(response.body) - expect(json['deleted']).to eq(false) - end - - it "doesn't return an error if delete_posts == true" do - delete :destroy, params: { id: delete_me.id, delete_posts: true }, format: :json - expect(response).to be_success - end - end - - it "deletes the user record" do - UserDestroyer.any_instance.expects(:destroy).returns(true) - delete :destroy, params: { id: delete_me.id }, format: :json - end - end - - context 'activate' do - before do - @reg_user = Fabricate(:inactive_user) - end - - it "returns success" do - put :activate, params: { user_id: @reg_user.id }, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json['success']).to eq("OK") - end - - it "should confirm email even when the tokens are expired" do - @reg_user.email_tokens.update_all(confirmed: false, expired: true) - - @reg_user.reload - expect(@reg_user.email_confirmed?).to eq(false) - - put :activate, params: { user_id: @reg_user.id }, format: :json - expect(response).to be_success - - @reg_user.reload - expect(@reg_user.email_confirmed?).to eq(true) - end - end - - context 'log_out' do - before do - @reg_user = Fabricate(:user) - end - - it "returns success" do - put :log_out, params: { user_id: @reg_user.id }, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json['success']).to eq("OK") - end - - it "returns 404 when user_id does not exist" do - put :log_out, params: { user_id: 123123 }, format: :json - expect(response).not_to be_success - end - end - - context 'silence' do - before do - @reg_user = Fabricate(:user) - end - - it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_silence_user?).with(@reg_user).returns(false) - put :silence, params: { user_id: @reg_user.id }, format: :json - expect(response).to be_forbidden - @reg_user.reload - expect(@reg_user).not_to be_silenced - end - - it "returns a 403 if the user doesn't exist" do - put :silence, params: { user_id: 123123 }, format: :json - expect(response).to be_forbidden - end - - it "punishes the user for spamming" do - put :silence, params: { user_id: @reg_user.id }, format: :json - expect(response).to be_success - @reg_user.reload - expect(@reg_user).to be_silenced - end - - it "can have an associated post" do - silence_post = Fabricate(:post, user: @reg_user) - - put :silence, params: { - user_id: @reg_user.id, - post_id: silence_post.id, - post_action: 'edit', - post_edit: "this is the new contents for the post" - }, format: :json - expect(response).to be_success - - silence_post.reload - expect(silence_post.raw).to eq("this is the new contents for the post") - - log = UserHistory.where( - target_user_id: @reg_user.id, - action: UserHistory.actions[:silence_user] - ).first - expect(log).to be_present - expect(log.post_id).to eq(silence_post.id) - - @reg_user.reload - expect(@reg_user).to be_silenced - end - - it "will set a length of time if provided" do - future_date = 1.month.from_now.to_date - put( - :silence, - params: { - user_id: @reg_user.id, - silenced_till: future_date - }, - format: :json - ) - @reg_user.reload - expect(@reg_user.silenced_till).to eq(future_date) - end - - it "will send a message if provided" do - Jobs.stubs(:enqueue) - Jobs.expects(:enqueue).with( - :critical_user_email, - has_entries( - type: :account_silenced, - user_id: @reg_user.id - ) - ) - - put( - :silence, - params: { - user_id: @reg_user.id, - message: "Email this to the user" - }, - format: :json - ) - end - end - - context 'unsilence' do - before do - @reg_user = Fabricate(:user) - end - - it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_unsilence_user?).with(@reg_user).returns(false) - put :unsilence, params: { user_id: @reg_user.id }, format: :json - expect(response).to be_forbidden - end - - it "returns a 403 if the user doesn't exist" do - put :unsilence, params: { user_id: 123123 }, format: :json - expect(response).to be_forbidden - end - - it "punishes the user for spamming" do - UserSilencer.expects(:unsilence).with(@reg_user, @user, anything) - put :unsilence, params: { user_id: @reg_user.id }, format: :json - end - end - - context 'ip-info' do - - it "uses ipinfo.io webservice to retrieve the info" do - Excon.expects(:get).with("https://ipinfo.io/123.123.123.123/json", read_timeout: 10, connect_timeout: 10) - get :ip_info, params: { ip: "123.123.123.123" }, format: :json - end - - end - - context "delete_other_accounts_with_same_ip" do - - it "works" do - Fabricate(:user, ip_address: "42.42.42.42") - Fabricate(:user, ip_address: "42.42.42.42") - - UserDestroyer.any_instance.expects(:destroy).twice - - delete :delete_other_accounts_with_same_ip, params: { - ip: "42.42.42.42", exclude: -1, order: "trust_level DESC" - }, format: :json - end - - end - - context ".invite_admin" do - it "doesn't work when not via API" do - controller.stubs(:is_api?).returns(false) - - post :invite_admin, params: { - name: 'Bill', username: 'bill22', email: 'bill@bill.com' - }, format: :json - - expect(response).not_to be_success - end - - it 'should invite admin' do - controller.stubs(:is_api?).returns(true) - Jobs.expects(:enqueue).with(:critical_user_email, anything).returns(true) - - post :invite_admin, params: { - name: 'Bill', username: 'bill22', email: 'bill@bill.com' - }, format: :json - - expect(response).to be_success - - u = User.find_by_email('bill@bill.com') - expect(u.name).to eq("Bill") - expect(u.username).to eq("bill22") - expect(u.admin).to eq(true) - end - - it "doesn't send the email with send_email falsy" do - controller.stubs(:is_api?).returns(true) - Jobs.expects(:enqueue).with(:user_email, anything).never - - post :invite_admin, params: { - name: 'Bill', username: 'bill22', email: 'bill@bill.com', send_email: '0' - }, format: :json - - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json["password_url"]).to be_present - end - end - - context 'remove_group' do - it "also clears the user's primary group" do - g = Fabricate(:group) - u = Fabricate(:user, primary_group: g) - delete :remove_group, params: { group_id: g.id, user_id: u.id }, format: :json - - expect(u.reload.primary_group).to be_nil - end - end - end - - context '#sync_sso' do - let(:sso) { SingleSignOn.new } - let(:sso_secret) { "sso secret" } - - before do - log_in(:admin) - - SiteSetting.email_editable = false - SiteSetting.sso_url = "https://www.example.com/sso" - SiteSetting.enable_sso = true - SiteSetting.sso_overrides_email = true - SiteSetting.sso_overrides_name = true - SiteSetting.sso_overrides_username = true - SiteSetting.sso_secret = sso_secret - sso.sso_secret = sso_secret - end - - it 'can sync up with the sso' do - sso.name = "Bob The Bob" - sso.username = "bob" - sso.email = "bob@bob.com" - sso.external_id = "1" - - user = DiscourseSingleSignOn.parse(sso.payload) - .lookup_or_create_user - - sso.name = "Bill" - sso.username = "Hokli$$!!" - sso.email = "bob2@bob.com" - - post :sync_sso, params: Rack::Utils.parse_query(sso.payload), format: :json - expect(response).to be_success - - user.reload - expect(user.email).to eq("bob2@bob.com") - expect(user.name).to eq("Bill") - expect(user.username).to eq("Hokli") - end - - it 'should create new users' do - sso.name = "Dr. Claw" - sso.username = "dr_claw" - sso.email = "dr@claw.com" - sso.external_id = "2" - post :sync_sso, params: Rack::Utils.parse_query(sso.payload), format: :json - expect(response).to be_success - - user = User.find_by_email('dr@claw.com') - expect(user).to be_present - expect(user.ip_address).to be_blank - end - - it 'should return the right message if the record is invalid' do - sso.email = "" - sso.name = "" - sso.external_id = "1" - - post :sync_sso, params: Rack::Utils.parse_query(sso.payload), format: :json - expect(response.status).to eq(403) - expect(JSON.parse(response.body)["message"]).to include("Primary email can't be blank") - end - end -end diff --git a/spec/controllers/categories_controller_spec.rb b/spec/controllers/categories_controller_spec.rb deleted file mode 100644 index 695f06c41b..0000000000 --- a/spec/controllers/categories_controller_spec.rb +++ /dev/null @@ -1,364 +0,0 @@ -require "rails_helper" - -describe CategoriesController do - describe "create" do - - it "requires the user to be logged in" do - post :create, format: :json - expect(response.status).to eq(403) - end - - describe "logged in" do - before do - SiteSetting.queue_jobs = false - @user = log_in(:admin) - end - - it "raises an exception when they don't have permission to create it" do - Guardian.any_instance.expects(:can_create?).with(Category, nil).returns(false) - post :create, params: { - name: 'hello', color: 'ff0', text_color: 'fff' - }, format: :json - - expect(response).to be_forbidden - end - - it "raises an exception when the name is missing" do - expect do - post :create, params: { color: "ff0", text_color: "fff" }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - it "raises an exception when the color is missing" do - expect do - post :create, params: { name: "hello", text_color: "fff" }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - it "raises an exception when the text color is missing" do - expect do - post :create, params: { name: "hello", color: "ff0" }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - describe "failure" do - before do - @category = Fabricate(:category, user: @user) - - post :create, params: { - name: @category.name, color: "ff0", text_color: "fff" - }, format: :json - end - - it "returns errors on a duplicate category name" do - expect(response.status).to eq(422) - end - end - - describe "success" do - it "works" do - readonly = CategoryGroup.permission_types[:readonly] - create_post = CategoryGroup.permission_types[:create_post] - - post :create, params: { - name: "hello", - color: "ff0", - text_color: "fff", - slug: "hello-cat", - auto_close_hours: 72, - permissions: { - "everyone" => readonly, - "staff" => create_post - } - }, format: :json - - expect(response.status).to eq(200) - category = Category.find_by(name: "hello") - expect(category.category_groups.map { |g| [g.group_id, g.permission_type] }.sort).to eq([ - [Group[:everyone].id, readonly], [Group[:staff].id, create_post] - ]) - expect(category.name).to eq("hello") - expect(category.slug).to eq("hello-cat") - expect(category.color).to eq("ff0") - expect(category.auto_close_hours).to eq(72) - expect(UserHistory.count).to eq(4) # 1 + 3 (bootstrap mode) - end - end - end - end - - describe "destroy" do - - it "requires the user to be logged in" do - delete :destroy, params: { id: "category" }, format: :json - expect(response.status).to eq(403) - end - - describe "logged in" do - before do - @user = log_in - @category = Fabricate(:category, user: @user) - end - - it "raises an exception if they don't have permission to delete it" do - Guardian.any_instance.expects(:can_delete_category?).returns(false) - delete :destroy, params: { id: @category.slug }, format: :json - expect(response).to be_forbidden - end - - it "deletes the record" do - Guardian.any_instance.expects(:can_delete_category?).returns(true) - expect do - delete :destroy, params: { id: @category.slug }, format: :json - end.to change(Category, :count).by(-1) - - expect(UserHistory.count).to eq(1) - end - end - - end - - describe "reorder" do - it "reorders the categories" do - admin = log_in(:admin) - - c1 = Fabricate(:category) - c2 = Fabricate(:category) - c3 = Fabricate(:category) - c4 = Fabricate(:category) - if c3.id < c2.id - tmp = c3; c2 = c3; c3 = tmp; - end - c1.position = 8 - c2.position = 6 - c3.position = 7 - c4.position = 5 - - payload = {} - payload[c1.id] = 4 - payload[c2.id] = 6 - payload[c3.id] = 6 - payload[c4.id] = 5 - - post :reorder, params: { mapping: MultiJson.dump(payload) }, format: :json - - SiteSetting.fixed_category_positions = true - list = CategoryList.new(Guardian.new(admin)) - - expect(list.categories).to eq([ - Category.find(SiteSetting.uncategorized_category_id), - c1, - c4, - c2, - c3 - ]) - end - end - - describe "update" do - before do - SiteSetting.queue_jobs = false - end - - it "requires the user to be logged in" do - put :update, params: { id: 'category' }, format: :json - expect(response.status).to eq(403) - end - - describe "logged in" do - let(:valid_attrs) { { id: @category.id, name: "hello", color: "ff0", text_color: "fff" } } - - before do - @user = log_in(:admin) - @category = Fabricate(:category, user: @user) - end - - it "raises an exception if they don't have permission to edit it" do - Guardian.any_instance.expects(:can_edit?).returns(false) - put :update, params: { - id: @category.slug, - name: 'hello', - color: 'ff0', - text_color: 'fff' - }, format: :json - - expect(response).to be_forbidden - end - - it "requires a name" do - expect do - put :update, params: { - id: @category.slug, - color: 'fff', - text_color: '0ff', - }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - it "requires a color" do - expect do - put :update, params: { - id: @category.slug, - name: 'asdf', - text_color: '0ff', - }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - it "requires a text color" do - expect do - put :update, - params: { id: @category.slug, name: 'asdf', color: 'fff' }, - format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - describe "failure" do - before do - @other_category = Fabricate(:category, name: "Other", user: @user) - - put :update, params: { - id: @category.id, - name: @other_category.name, - color: "ff0", - text_color: "fff", - }, format: :json - end - - it "returns errors on a duplicate category name" do - expect(response).not_to be_success - end - - it "returns errors on a duplicate category name" do - expect(response.code.to_i).to eq(422) - end - end - - it "returns 422 if email_in address is already in use for other category" do - @other_category = Fabricate(:category, name: "Other", email_in: "mail@examle.com") - - put :update, params: { - id: @category.id, - name: "Email", - email_in: "mail@examle.com", - color: "ff0", - text_color: "fff", - }, format: :json - - expect(response).not_to be_success - expect(response.code.to_i).to eq(422) - end - - describe "success" do - - it "updates the group correctly" do - readonly = CategoryGroup.permission_types[:readonly] - create_post = CategoryGroup.permission_types[:create_post] - - put :update, params: { - id: @category.id, - name: "hello", - color: "ff0", - text_color: "fff", - slug: "hello-category", - auto_close_hours: 72, - permissions: { - "everyone" => readonly, - "staff" => create_post - }, - custom_fields: { - "dancing" => "frogs" - }, - }, format: :json - - expect(response.status).to eq(200) - @category.reload - expect(@category.category_groups.map { |g| [g.group_id, g.permission_type] }.sort).to eq([ - [Group[:everyone].id, readonly], [Group[:staff].id, create_post] - ]) - expect(@category.name).to eq("hello") - expect(@category.slug).to eq("hello-category") - expect(@category.color).to eq("ff0") - expect(@category.auto_close_hours).to eq(72) - expect(@category.custom_fields).to eq("dancing" => "frogs") - end - - it 'logs the changes correctly' do - @category.update!(permissions: { "admins" => CategoryGroup.permission_types[:create_post] }) - - put :update, params: { - id: @category.id, - name: 'new name', - color: @category.color, - text_color: @category.text_color, - slug: @category.slug, - permissions: { - "everyone" => CategoryGroup.permission_types[:create_post] - }, - }, format: :json - - expect(UserHistory.count).to eq(5) # 2 + 3 (bootstrap mode) - end - end - end - - end - - describe 'update_slug' do - it 'requires the user to be logged in' do - put :update_slug, params: { category_id: 'category' }, format: :json - expect(response.status).to eq(403) - end - - describe 'logged in' do - let(:valid_attrs) { { id: @category.id, slug: 'fff' } } - - before do - @user = log_in(:admin) - @category = Fabricate(:happy_category, user: @user) - end - - it 'rejects blank' do - put :update_slug, params: { category_id: @category.id, slug: nil }, format: :json - expect(response.status).to eq(422) - end - - it 'accepts valid custom slug' do - put :update_slug, - params: { category_id: @category.id, slug: 'valid-slug' }, - format: :json - - expect(response).to be_success - expect(@category.reload.slug).to eq('valid-slug') - end - - it 'accepts not well formed custom slug' do - put :update_slug, - params: { category_id: @category.id, slug: ' valid slug' }, - format: :json - - expect(response).to be_success - expect(@category.reload.slug).to eq('valid-slug') - end - - it 'accepts and sanitize custom slug when the slug generation method is not ascii' do - SiteSetting.slug_generation_method = 'none' - put :update_slug, - params: { category_id: @category.id, slug: ' another !_ slug @' }, - format: :json - - expect(response).to be_success - expect(@category.reload.slug).to eq('another-slug') - SiteSetting.slug_generation_method = 'ascii' - end - - it 'rejects invalid custom slug' do - put :update_slug, - params: { category_id: @category.id, slug: ' ' }, - format: :json - - expect(response.status).to eq(422) - end - end - end -end diff --git a/spec/controllers/clicks_controller_spec.rb b/spec/controllers/clicks_controller_spec.rb deleted file mode 100644 index 67650b2eb8..0000000000 --- a/spec/controllers/clicks_controller_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'rails_helper' - -describe ClicksController do - - context 'create' do - - context 'missing params' do - - it 'raises a 404 without a url' do - get :track, params: { post_id: 123 } - - expect(response).to be_not_found - end - - end - - context 'correct params' do - let(:url) { "http://discourse.org" } - - before do - request.headers.merge!('REMOTE_ADDR' => '192.168.0.1') - end - - context "with a made up url" do - render_views - - it "doesn't redirect" do - TopicLinkClick.expects(:create_from).returns(nil) - - get :track, params: { url: 'http://discourse.org', post_id: 123 } - - expect(response).not_to be_redirect - expect(response.body).to include(I18n.t("redirect_warning")) - end - end - - context "with a valid url" do - it "redirects" do - TopicLinkClick.expects(:create_from).with(has_entries('url' => 'http://discourse.org/?hello=123')).returns(url) - - get :track, params: { url: 'http://discourse.org/?hello=123', post_id: 123 } - - expect(response).to redirect_to(url) - end - end - - context 'with a post_id' do - it 'redirects' do - TopicLinkClick.expects(:create_from).with('url' => url, 'post_id' => '123', 'ip' => '192.168.0.1').returns(url) - - get :track, params: { url: url, post_id: 123 } - - expect(response).to redirect_to(url) - end - - it "redirects links in whispers to staff members" do - log_in(:admin) - whisper = Fabricate(:post, post_type: Post.types[:whisper]) - - get :track, params: { url: url, post_id: whisper.id } - - expect(response).to redirect_to(url) - end - - it "doesn't redirect with the redirect=false param" do - TopicLinkClick.expects(:create_from).with('url' => url, 'post_id' => '123', 'ip' => '192.168.0.1', 'redirect' => 'false').returns(url) - - get :track, params: { url: url, post_id: 123, redirect: 'false' } - - expect(response).not_to be_redirect - end - end - - context 'with a topic_id' do - it 'redirects' do - TopicLinkClick.expects(:create_from).with('url' => url, 'topic_id' => '789', 'ip' => '192.168.0.1').returns(url) - - get :track, params: { url: url, topic_id: 789 } - - expect(response).to redirect_to(url) - end - end - - end - - end - -end diff --git a/spec/controllers/composer_messages_controller_spec.rb b/spec/controllers/composer_messages_controller_spec.rb deleted file mode 100644 index c2333fa33f..0000000000 --- a/spec/controllers/composer_messages_controller_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'rails_helper' - -describe ComposerMessagesController do - - context '.index' do - - it 'requires you to be logged in' do - get :index, format: :json - expect(response.status).to eq(403) - end - - context 'when logged in' do - let!(:user) { log_in } - let(:args) { { 'topic_id' => '123', 'post_id' => '333', 'composer_action' => 'reply' } } - - it 'redirects to your user preferences' do - get :index, format: :json - expect(response).to be_success - end - - it 'delegates args to the finder' do - finder = mock - ComposerMessagesFinder.expects(:new).with(instance_of(User), has_entries(args)).returns(finder) - finder.expects(:find) - get :index, params: args, format: :json - end - end - end -end diff --git a/spec/controllers/email_controller_spec.rb b/spec/controllers/email_controller_spec.rb deleted file mode 100644 index 4fdab4cca6..0000000000 --- a/spec/controllers/email_controller_spec.rb +++ /dev/null @@ -1,128 +0,0 @@ -require 'rails_helper' - -describe EmailController do - - context '.preferences_redirect' do - - it 'requires you to be logged in' do - get :preferences_redirect, format: :json - expect(response.status).to eq(403) - end - - context 'when logged in' do - let!(:user) { log_in } - - it 'redirects to your user preferences' do - get :preferences_redirect, format: :json - expect(response).to redirect_to("/u/#{user.username}/preferences") - end - end - - end - - context '.unsubscribe' do - - render_views - - it 'displays logo ut button if wrong user logged in' do - log_in_user Fabricate(:admin) - user = Fabricate(:user) - key = UnsubscribeKey.create_key_for(user, "digest") - - get :unsubscribe, params: { key: key } - - expect(response.body).to include(I18n.t("unsubscribe.log_out")) - expect(response.body).to include(I18n.t("unsubscribe.different_user_description")) - end - - it 'displays not found if key is not found' do - get :unsubscribe, params: { key: SecureRandom.hex } - expect(response.body).to include(CGI.escapeHTML(I18n.t("unsubscribe.not_found_description"))) - end - - it 'correctly handles mailing list mode' do - - user = Fabricate(:user) - key = UnsubscribeKey.create_key_for(user, "digest") - - user.user_option.update_columns(mailing_list_mode: true) - - get :unsubscribe, params: { key: key } - expect(response.body).to include(I18n.t("unsubscribe.mailing_list_mode")) - - SiteSetting.disable_mailing_list_mode = true - - get :unsubscribe, params: { key: key } - expect(response.body).not_to include(I18n.t("unsubscribe.mailing_list_mode")) - - user.user_option.update_columns(mailing_list_mode: false) - SiteSetting.disable_mailing_list_mode = false - - get :unsubscribe, params: { key: key } - expect(response.body).not_to include(I18n.t("unsubscribe.mailing_list_mode")) - - end - - it 'correctly handles digest unsubscribe' do - - user = Fabricate(:user) - user.user_option.update_columns(email_digests: false) - key = UnsubscribeKey.create_key_for(user, "digest") - - # because we are type digest we will always show digest and it will be selected - get :unsubscribe, params: { key: key } - expect(response.body).to include(I18n.t("unsubscribe.disable_digest_emails")) - - source = Nokogiri::HTML::fragment(response.body) - expect(source.css("#disable_digest_emails")[0]["checked"]).to eq("checked") - - SiteSetting.disable_digest_emails = true - - get :unsubscribe, params: { key: key } - expect(response.body).not_to include(I18n.t("unsubscribe.disable_digest_emails")) - - SiteSetting.disable_digest_emails = false - key = UnsubscribeKey.create_key_for(user, "not_digest") - - get :unsubscribe, params: { key: key } - expect(response.body).to include(I18n.t("unsubscribe.disable_digest_emails")) - end - - it 'correctly handles watched categories' do - post = Fabricate(:post) - user = post.user - cu = CategoryUser.create!(user_id: user.id, - category_id: post.topic.category_id, - notification_level: CategoryUser.notification_levels[:watching]) - - key = UnsubscribeKey.create_key_for(user, post) - get :unsubscribe, params: { key: key } - expect(response.body).to include("unwatch_category") - - cu.destroy! - - get :unsubscribe, params: { key: key } - expect(response.body).not_to include("unwatch_category") - - end - - it 'correctly handles watched first post categories' do - post = Fabricate(:post) - user = post.user - cu = CategoryUser.create!(user_id: user.id, - category_id: post.topic.category_id, - notification_level: CategoryUser.notification_levels[:watching_first_post]) - - key = UnsubscribeKey.create_key_for(user, post) - get :unsubscribe, params: { key: key } - expect(response.body).to include("unwatch_category") - - cu.destroy! - - get :unsubscribe, params: { key: key } - expect(response.body).not_to include("unwatch_category") - - end - end - -end diff --git a/spec/controllers/export_csv_controller_spec.rb b/spec/controllers/export_csv_controller_spec.rb deleted file mode 100644 index 2e1256653c..0000000000 --- a/spec/controllers/export_csv_controller_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require "rails_helper" - -describe ExportCsvController do - let(:export_filename) { "user-archive-codinghorror-150115-234817-999.csv.gz" } - - context "while logged in as normal user" do - before { @user = log_in(:user) } - - describe ".export_entity" do - it "enqueues export job" do - Jobs.expects(:enqueue).with(:export_csv_file, has_entries(entity: "user_archive", user_id: @user.id)) - post :export_entity, params: { entity: "user_archive" }, format: :json - expect(response).to be_success - end - - it "should not enqueue export job if rate limit is reached" do - Jobs::ExportCsvFile.any_instance.expects(:execute).never - UserExport.create(file_name: "user-archive-codinghorror-150116-003249", user_id: @user.id) - post :export_entity, params: { entity: "user_archive" }, format: :json - expect(response).not_to be_success - end - - it "returns 404 when normal user tries to export admin entity" do - post :export_entity, params: { entity: "staff_action" }, format: :json - expect(response).not_to be_success - end - end - end - - context "while logged in as an admin" do - before { @admin = log_in(:admin) } - - describe ".export_entity" do - it "enqueues export job" do - Jobs.expects(:enqueue).with(:export_csv_file, has_entries(entity: "staff_action", user_id: @admin.id)) - post :export_entity, params: { entity: "staff_action" }, format: :json - expect(response).to be_success - end - - it "should not rate limit export for staff" do - Jobs.expects(:enqueue).with(:export_csv_file, has_entries(entity: "staff_action", user_id: @admin.id)) - UserExport.create(file_name: "screened-email-150116-010145", user_id: @admin.id) - post :export_entity, params: { entity: "staff_action" }, format: :json - expect(response).to be_success - end - end - end -end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb deleted file mode 100644 index 6433a2de7f..0000000000 --- a/spec/controllers/invites_controller_spec.rb +++ /dev/null @@ -1,396 +0,0 @@ -require 'rails_helper' - -describe InvitesController do - - context '.destroy' do - - it 'requires you to be logged in' do - delete :destroy, - params: { email: 'jake@adventuretime.ooo' }, - format: :json - expect(response.status).to eq(403) - end - - context 'while logged in' do - let!(:user) { log_in } - let!(:invite) { Fabricate(:invite, invited_by: user) } - let(:another_invite) { Fabricate(:invite, email: 'anotheremail@address.com') } - - it 'raises an error when the email is missing' do - expect { delete :destroy, format: :json }.to raise_error(ActionController::ParameterMissing) - end - - it "raises an error when the email cannot be found" do - delete :destroy, params: { email: 'finn@adventuretime.ooo' }, format: :json - expect(response.status).to eq(400) - end - - it 'raises an error when the invite is not yours' do - delete :destroy, params: { email: another_invite.email }, format: :json - expect(response.status).to eq(400) - end - - it "destroys the invite" do - Invite.any_instance.expects(:trash!).with(user) - delete :destroy, params: { email: invite.email }, format: :json - end - - end - - end - - context '#create' do - it 'requires you to be logged in' do - post :create, params: { email: 'jake@adventuretime.ooo' }, format: :json - expect(response.status).to eq(403) - end - - context 'while logged in' do - let(:email) { 'jake@adventuretime.ooo' } - - it "fails if you can't invite to the forum" do - log_in - post :create, params: { email: email }, format: :json - expect(response).not_to be_success - end - - it "fails for normal user if invite email already exists" do - user = log_in(:trust_level_4) - invite = Invite.invite_by_email("invite@example.com", user) - invite.reload - post :create, params: { email: invite.email }, format: :json - expect(response).not_to be_success - json = JSON.parse(response.body) - expect(json["failed"]).to be_present - end - - it "allows admins to invite to groups" do - group = Fabricate(:group) - log_in(:admin) - post :create, params: { email: email, group_names: group.name }, format: :json - expect(response).to be_success - expect(Invite.find_by(email: email).invited_groups.count).to eq(1) - end - - it 'allows group owners to invite to groups' do - group = Fabricate(:group) - user = log_in - user.update!(trust_level: TrustLevel[2]) - group.add_owner(user) - - post :create, params: { email: email, group_names: group.name }, format: :json - - expect(response).to be_success - expect(Invite.find_by(email: email).invited_groups.count).to eq(1) - end - - it "allows admin to send multiple invites to same email" do - user = log_in(:admin) - invite = Invite.invite_by_email("invite@example.com", user) - invite.reload - post :create, params: { email: invite.email }, format: :json - expect(response).to be_success - end - - it "responds with error message in case of validation failure" do - log_in(:admin) - post :create, params: { email: "test@mailinator.com" }, format: :json - expect(response).not_to be_success - json = JSON.parse(response.body) - expect(json["errors"]).to be_present - end - end - - end - - context '.create_invite_link' do - it 'requires you to be logged in' do - post :create_invite_link, params: { - email: 'jake@adventuretime.ooo' - }, format: :json - expect(response.status).to eq(403) - end - - context 'while logged in' do - let(:email) { 'jake@adventuretime.ooo' } - - it "fails if you can't invite to the forum" do - log_in - post :create_invite_link, params: { email: email }, format: :json - expect(response).not_to be_success - end - - it "fails for normal user if invite email already exists" do - user = log_in(:trust_level_4) - invite = Invite.invite_by_email("invite@example.com", user) - invite.reload - - post :create_invite_link, params: { - email: invite.email - }, format: :json - - expect(response).not_to be_success - end - - it "verifies that inviter is authorized to invite new user to a group-private topic" do - group = Fabricate(:group) - private_category = Fabricate(:private_category, group: group) - group_private_topic = Fabricate(:topic, category: private_category) - log_in(:trust_level_4) - - post :create_invite_link, params: { - email: email, topic_id: group_private_topic.id - }, format: :json - - expect(response).not_to be_success - end - - it "allows admins to invite to groups" do - group = Fabricate(:group) - log_in(:admin) - - post :create_invite_link, params: { - email: email, group_names: group.name - }, format: :json - - expect(response).to be_success - expect(Invite.find_by(email: email).invited_groups.count).to eq(1) - end - - it "allows multiple group invite" do - Fabricate(:group, name: "security") - Fabricate(:group, name: "support") - log_in(:admin) - - post :create_invite_link, params: { - email: email, group_names: "security,support" - }, format: :json - - expect(response).to be_success - expect(Invite.find_by(email: email).invited_groups.count).to eq(2) - end - end - end - - context '.perform_accept_invitation' do - - context 'with an invalid invite id' do - before do - put :perform_accept_invitation, params: { id: "doesn't exist" }, format: :json - end - - it "redirects to the root" do - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["success"]).to eq(false) - expect(json["message"]).to eq(I18n.t('invite.not_found')) - end - - it "should not change the session" do - expect(session[:current_user_id]).to be_blank - end - end - - context 'with a deleted invite' do - let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } - let(:deleted_invite) { invite.destroy; invite } - before do - put :perform_accept_invitation, params: { id: deleted_invite.invite_key }, format: :json - end - - it "redirects to the root" do - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["success"]).to eq(false) - expect(json["message"]).to eq(I18n.t('invite.not_found')) - end - - it "should not change the session" do - expect(session[:current_user_id]).to be_blank - end - end - - context 'with a valid invite id' do - let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } - - it 'redeems the invite' do - Invite.any_instance.expects(:redeem) - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - - context 'when redeem returns a user' do - let(:user) { Fabricate(:coding_horror) } - - context 'success' do - subject { put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json } - - before do - Invite.any_instance.expects(:redeem).returns(user) - end - - it 'logs in the user' do - events = DiscourseEvent.track_events { subject } - - expect(events.map { |event| event[:event_name] }).to include( - :user_logged_in, :user_first_logged_in - ) - - expect(session[:current_user_id]).to eq(user.id) - end - - it 'redirects to the first topic the user was invited to' do - subject - json = JSON.parse(response.body) - expect(json["success"]).to eq(true) - expect(json["redirect_to"]).to eq(topic.relative_url) - end - end - - context 'failure' do - subject { put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json } - - it "doesn't log in the user if there's a validation error" do - user.errors.add(:password, :common) - Invite.any_instance.expects(:redeem).raises(ActiveRecord::RecordInvalid.new(user)) - subject - expect(response).to be_success - json = JSON.parse(response.body) - expect(json["success"]).to eq(false) - expect(json["errors"]["password"]).to be_present - end - end - - context '.post_process_invite' do - before do - Invite.any_instance.stubs(:redeem).returns(user) - Jobs.expects(:enqueue).with(:invite_email, has_key(:invite_id)) - user.password_hash = nil - end - - it 'sends a welcome message if set' do - user.send_welcome_message = true - user.expects(:enqueue_welcome_message).with('welcome_invite') - Jobs.expects(:enqueue).with(:invite_password_instructions_email, has_entries(username: user.username)) - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - - it "sends password reset email if password is not set" do - user.expects(:enqueue_welcome_message).with('welcome_invite').never - Jobs.expects(:enqueue).with(:invite_password_instructions_email, has_entries(username: user.username)) - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - - it "does not send password reset email if sso is enabled" do - SiteSetting.sso_url = "https://www.example.com/sso" - SiteSetting.enable_sso = true - Jobs.expects(:enqueue).with(:invite_password_instructions_email, has_key(:username)).never - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - - it "does not send password reset email if local login is disabled" do - SiteSetting.enable_local_logins = false - Jobs.expects(:enqueue).with(:invite_password_instructions_email, has_key(:username)).never - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - - it 'sends an activation email if password is set' do - user.password_hash = 'qaw3ni3h2wyr63lakw7pea1nrtr44pls' - Jobs.expects(:enqueue).with(:invite_password_instructions_email, has_key(:username)).never - Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup, user_id: user.id)) - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - end - end - end - - context 'new registrations are disabled' do - let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } - before { SiteSetting.allow_new_registrations = false } - - it "doesn't redeem the invite" do - Invite.any_instance.stubs(:redeem).never - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - end - - context 'user is already logged in' do - let!(:user) { log_in } - let(:topic) { Fabricate(:topic) } - let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } - - it "doesn't redeem the invite" do - Invite.any_instance.stubs(:redeem).never - put :perform_accept_invitation, params: { id: invite.invite_key }, format: :json - end - end - end - - context '.resend_invite' do - - it 'requires you to be logged in' do - delete :resend_invite, params: { email: 'first_name@example.com' }, format: :json - expect(response.status).to eq(403) - end - - context 'while logged in' do - let!(:user) { log_in } - let!(:invite) { Fabricate(:invite, invited_by: user) } - let(:another_invite) { Fabricate(:invite, email: 'last_name@example.com') } - - it 'raises an error when the email is missing' do - expect { post :resend_invite, format: :json }.to raise_error(ActionController::ParameterMissing) - end - - it "raises an error when the email cannot be found" do - post :resend_invite, params: { email: 'first_name@example.com' }, format: :json - expect(response.status).to eq(400) - end - - it 'raises an error when the invite is not yours' do - post :resend_invite, params: { email: another_invite.email }, format: :json - expect(response.status).to eq(400) - end - - it "resends the invite" do - Invite.any_instance.expects(:resend_invite) - post :resend_invite, params: { email: invite.email }, format: :json - end - - end - - end - - context '.upload_csv' do - it 'requires you to be logged in' do - post :upload_csv, format: :json - expect(response.status).to eq(403) - end - - context 'while logged in' do - let(:csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/discourse.csv") } - - let(:file) do - Rack::Test::UploadedFile.new(File.open(csv_file)) - end - - let(:filename) { 'discourse.csv' } - - it "fails if you can't bulk invite to the forum" do - log_in - post :upload_csv, params: { file: file, name: filename }, format: :json - expect(response).not_to be_success - end - - it "allows admin to bulk invite" do - log_in(:admin) - post :upload_csv, params: { file: file, name: filename }, format: :json - expect(response).to be_success - end - end - - end - -end diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb deleted file mode 100644 index 1f1374c5c9..0000000000 --- a/spec/controllers/list_controller_spec.rb +++ /dev/null @@ -1,410 +0,0 @@ -require 'rails_helper' - -describe ListController do - - # we need some data - before do - @user = Fabricate(:coding_horror) - @post = Fabricate(:post, user: @user) - - SiteSetting.top_menu = 'latest|new|unread|categories' - end - - describe 'indexes' do - - (Discourse.anonymous_filters - [:categories]).each do |filter| - context "#{filter}" do - before { get filter } - it { is_expected.to respond_with(:success) } - end - end - - it 'allows users to filter on a set of topic ids' do - p = create_post - - get :latest, format: :json, params: { topic_ids: "#{p.topic_id}" } - expect(response).to be_success - parsed = JSON.parse(response.body) - expect(parsed["topic_list"]["topics"].length).to eq(1) - end - end - - describe 'RSS feeds' do - it 'renders latest RSS' do - get "latest_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - - it 'renders top RSS' do - get "top_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - - it 'renders all time top RSS' do - get "top_all_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - - it 'renders yearly top RSS' do - get "top_yearly_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - - it 'renders quarterly top RSS' do - get "top_quarterly_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - - it 'renders monthly top RSS' do - get "top_monthly_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - - it 'renders weekly top RSS' do - get "top_weekly_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - - it 'renders daily top RSS' do - get "top_daily_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - end - - context 'category' do - - context 'in a category' do - let(:category) { Fabricate(:category) } - - context 'without access to see the category' do - before do - Guardian.any_instance.expects(:can_see?).with(category).returns(false) - get :category_latest, params: { category: category.slug } - end - - it { is_expected.not_to respond_with(:success) } - end - - context 'with access to see the category' do - before do - get :category_latest, params: { category: category.slug } - end - - it { is_expected.to respond_with(:success) } - end - - context 'with a link that includes an id' do - before do - get :category_latest, params: { - category: "#{category.id}-#{category.slug}" - } - end - - it { is_expected.to respond_with(:success) } - end - - context 'with a link that has a parent slug, slug and id in its path' do - let(:child_category) { Fabricate(:category, parent_category: category) } - - context "with valid slug" do - before do - get :category_latest, params: { - parent_category: category.slug, - category: child_category.slug, - id: child_category.id - } - end - - it { is_expected.to redirect_to(child_category.url) } - end - - context "with invalid slug" do - before do - get :category_latest, params: { - parent_category: 'random slug', - category: 'random slug', - id: child_category.id - } - end - - it { is_expected.to redirect_to(child_category.url) } - end - end - - context 'another category exists with a number at the beginning of its name' do - # One category has another category's id at the beginning of its name - let!(:other_category) { Fabricate(:category, name: "#{category.id} name") } - - it 'uses the correct category' do - get :category_latest, - params: { category: other_category.slug }, - format: :json - - expect(response).to be_success - - body = JSON.parse(response.body) - - expect(body["topic_list"]["topics"].first["category_id"]) - .to eq(other_category.id) - end - end - - context 'a child category' do - let(:sub_category) { Fabricate(:category, parent_category_id: category.id) } - - context 'when parent and child are requested' do - before do - get :category_latest, params: { - parent_category: category.slug, category: sub_category.slug - } - end - - it { is_expected.to respond_with(:success) } - end - - context 'when child is requested with the wrong parent' do - before do - get :category_latest, params: { - parent_category: 'not_the_right_slug', category: sub_category.slug - } - end - - it { is_expected.not_to respond_with(:success) } - end - end - - describe 'feed' do - it 'renders RSS' do - get :category_feed, params: { category: category.slug }, format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - end - - describe "category default views" do - it "has a top default view" do - category.update_attributes!(default_view: 'top', default_top_period: 'monthly') - described_class.expects(:best_period_with_topics_for).with(anything, category.id, :monthly).returns(:monthly) - get :category_default, params: { category: category.slug } - expect(response).to be_success - end - - it "has a default view of nil" do - category.update_attributes!(default_view: nil) - described_class.expects(:best_period_for).never - get :category_default, params: { category: category.slug } - expect(response).to be_success - end - - it "has a default view of ''" do - category.update_attributes!(default_view: '') - described_class.expects(:best_period_for).never - get :category_default, params: { category: category.slug } - expect(response).to be_success - end - - it "has a default view of latest" do - category.update_attributes!(default_view: 'latest') - described_class.expects(:best_period_for).never - get :category_default, params: { category: category.slug } - expect(response).to be_success - end - end - - describe "renders canonical tag" do - render_views - - it 'for category default view' do - get :category_default, params: { category: category.slug } - expect(response).to be_success - expect(css_select("link[rel=canonical]").length).to eq(1) - end - - it 'for category latest view' do - get :category_latest, params: { category: category.slug } - expect(response).to be_success - expect(css_select("link[rel=canonical]").length).to eq(1) - end - end - end - end - - describe "topics_by" do - let!(:user) { log_in } - - it "should respond with a list" do - get :topics_by, params: { username: @user.username } - expect(response).to be_success - end - end - - context "private_messages" do - let!(:user) { log_in } - - it "raises an error when can_see_private_messages? is false " do - Guardian.any_instance.expects(:can_see_private_messages?).returns(false) - get :private_messages, params: { username: @user.username } - expect(response).to be_forbidden - end - - it "succeeds when can_see_private_messages? is false " do - Guardian.any_instance.expects(:can_see_private_messages?).returns(true) - get :private_messages, params: { username: @user.username } - expect(response).to be_success - end - end - - context "private_messages_sent" do - let!(:user) { log_in } - - it "raises an error when can_see_private_messages? is false " do - Guardian.any_instance.expects(:can_see_private_messages?).returns(false) - get :private_messages_sent, params: { username: @user.username } - expect(response).to be_forbidden - end - - it "succeeds when can_see_private_messages? is false " do - Guardian.any_instance.expects(:can_see_private_messages?).returns(true) - get :private_messages_sent, params: { username: @user.username } - expect(response).to be_success - end - end - - context "private_messages_unread" do - let!(:user) { log_in } - - it "raises an error when can_see_private_messages? is false " do - Guardian.any_instance.expects(:can_see_private_messages?).returns(false) - get :private_messages_unread, params: { username: @user.username } - expect(response).to be_forbidden - end - - it "succeeds when can_see_private_messages? is false " do - Guardian.any_instance.expects(:can_see_private_messages?).returns(true) - get :private_messages_unread, params: { username: @user.username } - expect(response).to be_success - end - end - - context 'read' do - it 'raises an error when not logged in' do - get :read - expect(response.status).to eq(404) - end - - context 'when logged in' do - before do - log_in_user(@user) - get :read - end - - it { is_expected.to respond_with(:success) } - end - end - - describe "best_periods_for" do - - it "returns yearly for more than 180 days" do - expect(ListController.best_periods_for(nil, :all)).to eq([:yearly]) - expect(ListController.best_periods_for(180.days.ago, :all)).to eq([:yearly]) - end - - it "includes monthly when less than 180 days and more than 35 days" do - (35...180).each do |date| - expect(ListController.best_periods_for(date.days.ago, :all)).to eq([:monthly, :yearly]) - end - end - - it "includes weekly when less than 35 days and more than 8 days" do - (8...35).each do |date| - expect(ListController.best_periods_for(date.days.ago, :all)).to eq([:weekly, :monthly, :yearly]) - end - end - - it "includes daily when less than 8 days" do - (0...8).each do |date| - expect(ListController.best_periods_for(date.days.ago, :all)).to eq([:daily, :weekly, :monthly, :yearly]) - end - end - - it "returns default even for more than 180 days" do - expect(ListController.best_periods_for(nil, :monthly)).to eq([:monthly, :yearly]) - expect(ListController.best_periods_for(180.days.ago, :monthly)).to eq([:monthly, :yearly]) - end - - it "returns default even when less than 180 days and more than 35 days" do - (35...180).each do |date| - expect(ListController.best_periods_for(date.days.ago, :weekly)).to eq([:weekly, :monthly, :yearly]) - end - end - - it "returns default even when less than 35 days and more than 8 days" do - (8...35).each do |date| - expect(ListController.best_periods_for(date.days.ago, :daily)).to eq([:daily, :weekly, :monthly, :yearly]) - end - end - - it "doesn't return default when set to all" do - expect(ListController.best_periods_for(nil, :all)).to eq([:yearly]) - end - - it "doesn't return value twice when matches default" do - expect(ListController.best_periods_for(nil, :yearly)).to eq([:yearly]) - end - end - - describe "categories suppression" do - let(:category_one) { Fabricate(:category) } - let(:sub_category) { Fabricate(:category, parent_category: category_one, suppress_from_latest: true) } - let!(:topic_in_sub_category) { Fabricate(:topic, category: sub_category) } - - let(:category_two) { Fabricate(:category, suppress_from_latest: true) } - let!(:topic_in_category_two) { Fabricate(:topic, category: category_two) } - - it "suppresses categories from the latest list" do - get SiteSetting.homepage, format: :json - expect(response).to be_success - - topic_titles = JSON.parse(response.body)["topic_list"]["topics"].map { |t| t["title"] } - expect(topic_titles).not_to include(topic_in_sub_category.title, topic_in_category_two.title) - end - - it "does not suppress" do - get SiteSetting.homepage, params: { category: category_one.id }, format: :json - expect(response).to be_success - - topic_titles = JSON.parse(response.body)["topic_list"]["topics"].map { |t| t["title"] } - expect(topic_titles).to include(topic_in_sub_category.title) - end - - end - - describe "safe mode" do - render_views - - it "handles safe mode" do - get :latest - expect(response.body).to match(/plugin\.js/) - expect(response.body).to match(/plugin-third-party\.js/) - - get :latest, params: { safe_mode: "no_plugins" } - expect(response.body).not_to match(/plugin\.js/) - expect(response.body).not_to match(/plugin-third-party\.js/) - - get :latest, params: { safe_mode: "only_official" } - expect(response.body).to match(/plugin\.js/) - expect(response.body).not_to match(/plugin-third-party\.js/) - - end - - end - -end diff --git a/spec/controllers/onebox_controller_spec.rb b/spec/controllers/onebox_controller_spec.rb deleted file mode 100644 index 9e2008c848..0000000000 --- a/spec/controllers/onebox_controller_spec.rb +++ /dev/null @@ -1,162 +0,0 @@ -require 'rails_helper' - -describe OneboxController do - - let(:url) { "http://google.com" } - - it "requires the user to be logged in" do - get :show, params: { url: url }, format: :json - expect(response.status).to eq(403) - end - - describe "logged in" do - - before { @user = log_in(:admin) } - - it 'invalidates the cache if refresh is passed' do - Oneboxer.expects(:preview).with(url, invalidate_oneboxes: true, user_id: @user.id, category_id: 0, topic_id: 0) - get :show, params: { url: url, refresh: 'true' }, format: :json - end - - describe "cached onebox" do - - it "returns the cached onebox response in the body" do - onebox_html = <<~HTML - - - - - - -

body

- - - HTML - - url = "http://noodle.com/" - - stub_request(:head, url) - stub_request(:get, url).to_return(body: onebox_html).then.to_raise - - get :show, params: { url: url, refresh: "true" }, format: :json - - expect(response).to be_success - expect(response.body).to include('Fred') - expect(response.body).to include('bodycontent') - - get :show, params: { url: url }, format: :json - expect(response).to be_success - expect(response.body).to include('Fred') - expect(response.body).to include('bodycontent') - end - - end - - describe "only 1 outgoing preview per user" do - - it "returns 429" do - Oneboxer.expects(:is_previewing?).returns(true) - get :show, params: { url: url }, format: :json - expect(response.status).to eq(429) - end - - end - - describe "found onebox" do - - let(:body) { "this is the onebox body" } - - before do - Oneboxer.expects(:preview).returns(body) - get :show, params: { url: url }, format: :json - end - - it 'returns the onebox response in the body' do - expect(response).to be_success - expect(response.body).to eq(body) - end - - end - - describe "missing onebox" do - - it "returns 404 if the onebox is nil" do - Oneboxer.expects(:preview).returns(nil) - get :show, params: { url: url }, format: :json - expect(response.response_code).to eq(404) - end - - it "returns 404 if the onebox is an empty string" do - Oneboxer.expects(:preview).returns(" \t ") - get :show, params: { url: url }, format: :json - expect(response.response_code).to eq(404) - end - - end - - describe "local onebox" do - - it 'does not cache local oneboxes' do - post = create_post - url = Discourse.base_url + post.url - - get :show, params: { url: url, category_id: post.topic.category_id }, format: :json - expect(response.body).to include('blockquote') - - post.trash! - - get :show, params: { url: url, category_id: post.topic.category_id }, format: :json - expect(response.body).not_to include('blockquote') - end - end - - it 'does not onebox when you have no permission on category' do - log_in - - post = create_post - url = Discourse.base_url + post.url - - get :show, params: { url: url, category_id: post.topic.category_id }, format: :json - expect(response.body).to include('blockquote') - - post.topic.category.set_permissions(staff: :full) - post.topic.category.save - - get :show, params: { url: url, category_id: post.topic.category_id }, format: :json - expect(response.body).not_to include('blockquote') - end - - it 'does not allow onebox of PMs' do - user = log_in - - post = create_post(archetype: 'private_message', target_usernames: [user.username]) - url = Discourse.base_url + post.url - - get :show, params: { url: url }, format: :json - expect(response.body).not_to include('blockquote') - end - - it 'does not allow whisper onebox' do - log_in - - post = create_post - whisper = create_post(topic_id: post.topic_id, post_type: Post.types[:whisper]) - url = Discourse.base_url + whisper.url - - get :show, params: { url: url }, format: :json - expect(response.body).not_to include('blockquote') - end - - it 'allows onebox to public topics/posts in PM' do - log_in - - post = create_post - url = Discourse.base_url + post.url - - get :show, params: { url: url }, format: :json - expect(response.body).to include('blockquote') - end - - end - -end diff --git a/spec/controllers/post_actions_controller_spec.rb b/spec/controllers/post_actions_controller_spec.rb deleted file mode 100644 index 0994fc601d..0000000000 --- a/spec/controllers/post_actions_controller_spec.rb +++ /dev/null @@ -1,135 +0,0 @@ -require 'rails_helper' - -describe PostActionsController do - - describe 'create' do - - context 'logged in as user' do - let(:user) { Fabricate(:user) } - let(:private_message) { Fabricate(:private_message_post, user: Fabricate(:coding_horror)) } - - before do - log_in_user(user) - end - - it 'fails when the user does not have permission to see the post' do - post :create, params: { - id: private_message.id, - post_action_type_id: PostActionType.types[:bookmark] - }, format: :json - - expect(response).to be_forbidden - end - end - end - - context 'destroy' do - - let(:post) { Fabricate(:post, user: Fabricate(:coding_horror)) } - - context 'logged in' do - let!(:user) { log_in } - - it 'raises an error when the post_action_type_id is missing' do - expect do - delete :destroy, params: { id: post.id }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - it "returns 404 when the post action type doesn't exist for that user" do - delete :destroy, params: { id: post.id, post_action_type_id: 1 }, format: :json - expect(response.code).to eq('404') - end - - context 'with a post_action record ' do - let!(:post_action) { PostAction.create(user_id: user.id, post_id: post.id, post_action_type_id: 1) } - - it 'returns success' do - delete :destroy, params: { id: post.id, post_action_type_id: 1 }, format: :json - expect(response).to be_success - end - - it 'deletes the action' do - delete :destroy, params: { - id: post.id, post_action_type_id: 1 - }, format: :json - - expect(PostAction.exists?(user_id: user.id, post_id: post.id, post_action_type_id: 1, deleted_at: nil)).to eq(false) - end - - it 'ensures it can be deleted' do - Guardian.any_instance.expects(:can_delete?).with(post_action).returns(false) - - delete :destroy, params: { - id: post.id, post_action_type_id: 1 - }, format: :json - - expect(response).to be_forbidden - end - end - - end - - end - - context 'defer_flags' do - - let(:flagged_post) { Fabricate(:post, user: Fabricate(:coding_horror)) } - - context "not logged in" do - it "should not allow them to clear flags" do - post :defer_flags, format: :json - expect(response.status).to eq(403) - end - end - - context 'logged in' do - let!(:user) { log_in(:moderator) } - - it "raises an error without a post_action_type_id" do - expect do - post :defer_flags, params: { id: flagged_post.id }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - it "raises an error when the user doesn't have access" do - Guardian.any_instance.expects(:can_defer_flags?).returns(false) - - post :defer_flags, params: { - id: flagged_post.id, post_action_type_id: PostActionType.types[:spam] - }, format: :json - - expect(response).to be_forbidden - end - - context "success" do - before do - Guardian.any_instance.expects(:can_defer_flags?).returns(true) - PostAction.expects(:defer_flags!).with(flagged_post, user) - end - - it "delegates to defer_flags" do - post :defer_flags, params: { - id: flagged_post.id, post_action_type_id: PostActionType.types[:spam] - }, format: :json - - expect(response).to be_success - end - - it "works with a deleted post" do - flagged_post.trash!(user) - - post :defer_flags, params: { - id: flagged_post.id, post_action_type_id: PostActionType.types[:spam] - }, format: :json - - expect(response).to be_success - end - - end - - end - - end - -end diff --git a/spec/controllers/queued_posts_controller_spec.rb b/spec/controllers/queued_posts_controller_spec.rb deleted file mode 100644 index ac88c50479..0000000000 --- a/spec/controllers/queued_posts_controller_spec.rb +++ /dev/null @@ -1,144 +0,0 @@ -require 'rails_helper' -require_dependency 'queued_posts_controller' -require_dependency 'queued_post' - -describe QueuedPostsController do - context 'without authentication' do - it 'fails' do - get :index, format: :json - expect(response).not_to be_success - end - end - - context 'as a regular user' do - let!(:user) { log_in(:user) } - it 'fails' do - get :index, format: :json - expect(response).not_to be_success - end - end - - context 'as an admin' do - let!(:user) { log_in(:moderator) } - - it 'returns the queued posts' do - get :index, format: :json - expect(response).to be_success - end - end - - describe '#update' do - let!(:user) { log_in(:moderator) } - let(:qp) { Fabricate(:queued_post) } - - context 'not found' do - it 'returns json error' do - qp.destroy! - - put :update, params: { - id: qp.id, queued_post: { state: 'approved' } - }, format: :json - - expect(response.status).to eq(422) - - expect(eval(response.body)).to eq(described_class.new.create_errors_json(I18n.t('queue.not_found'))) - end - end - - context 'approved' do - it 'updates the post to approved' do - - put :update, params: { - id: qp.id, queued_post: { state: 'approved' } - }, format: :json - - expect(response).to be_success - - qp.reload - expect(qp.state).to eq(QueuedPost.states[:approved]) - end - end - - context 'rejected' do - it 'updates the post to rejected' do - - put :update, params: { - id: qp.id, queued_post: { state: 'rejected' } - }, format: :json - - expect(response).to be_success - - qp.reload - expect(qp.state).to eq(QueuedPost.states[:rejected]) - end - end - - context 'editing content' do - let(:changes) do - { - raw: 'new raw', - title: 'new title', - category_id: 10, - tags: ['new_tag'] - } - end - - context 'when it is a topic' do - let(:queued_topic) { Fabricate(:queued_topic) } - - before do - put :update, params: { - id: queued_topic.id, queued_post: changes - }, format: :json - - expect(response).to be_success - end - - it 'updates raw' do - expect(queued_topic.reload.raw).to eq(changes[:raw]) - end - - it 'updates the title' do - expect(queued_topic.reload.post_options['title']).to eq(changes[:title]) - end - - it 'updates the category' do - expect(queued_topic.reload.post_options['category']).to eq(changes[:category_id]) - end - - it 'updates the tags' do - expect(queued_topic.reload.post_options['tags']).to eq(changes[:tags]) - end - end - - context 'when it is a reply' do - let(:queued_reply) { Fabricate(:queued_post) } - - before do - put :update, params: { - id: queued_reply.id, queued_post: changes - }, format: :json - - expect(response).to be_success - end - - it 'updates raw' do - expect(queued_reply.reload.raw).to eq(changes[:raw]) - end - - it 'does not update the title' do - expect(queued_reply.reload.post_options['title']).to be_nil - end - - it 'does not update the category' do - original_category = queued_reply.post_options['category'] - expect(queued_reply.reload.post_options['category']).to eq(original_category) - end - - it 'does not update the tags' do - expect(queued_reply.reload.post_options['tags']).to be_nil - end - end - end - end -end diff --git a/spec/controllers/similar_topics_controller_spec.rb b/spec/controllers/similar_topics_controller_spec.rb deleted file mode 100644 index f7dad39562..0000000000 --- a/spec/controllers/similar_topics_controller_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'rails_helper' - -describe SimilarTopicsController do - context 'similar_to' do - - let(:title) { 'this title is long enough to search for' } - let(:raw) { 'this body is long enough to search for' } - - it "requires a title" do - expect do - get :index, params: { raw: raw }, format: :json - end.to raise_error(ActionController::ParameterMissing) - end - - it "returns no results if the title length is below the minimum" do - Topic.expects(:similar_to).never - SiteSetting.min_title_similar_length = 100 - get :index, params: { title: title, raw: raw }, format: :json - json = ::JSON.parse(response.body) - expect(json["similar_topics"].size).to eq(0) - end - - describe "minimum_topics_similar" do - - before do - SiteSetting.minimum_topics_similar = 30 - end - - after do - get :index, params: { title: title, raw: raw }, format: :json - end - - describe "With enough topics" do - before do - Topic.stubs(:count).returns(50) - end - - it "deletes to Topic.similar_to if there are more topics than `minimum_topics_similar`" do - Topic.expects(:similar_to).with(title, raw, nil).returns([Fabricate(:topic)]) - end - - describe "with a logged in user" do - let(:user) { log_in } - - it "passes a user through if logged in" do - Topic.expects(:similar_to).with(title, raw, user).returns([Fabricate(:topic)]) - end - end - - end - - it "does not call Topic.similar_to if there are fewer topics than `minimum_topics_similar`" do - Topic.stubs(:count).returns(10) - Topic.expects(:similar_to).never - end - - end - - end - -end diff --git a/spec/controllers/steps_controller_spec.rb b/spec/controllers/steps_controller_spec.rb deleted file mode 100644 index ec1f7fd608..0000000000 --- a/spec/controllers/steps_controller_spec.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'rails_helper' - -describe StepsController do - - before do - SiteSetting.wizard_enabled = true - end - - it 'needs you to be logged in' do - put :update, params: { - id: 'made-up-id', fields: { forum_title: "updated title" } - }, format: :json - expect(response.status).to eq(403) - end - - it "raises an error if you aren't an admin" do - log_in(:moderator) - - put :update, params: { - id: 'made-up-id', fields: { forum_title: "updated title" } - }, format: :json - - expect(response).to be_forbidden - end - - context "as an admin" do - before do - log_in(:admin) - end - - it "raises an error if the wizard is disabled" do - SiteSetting.wizard_enabled = false - put :update, params: { - id: 'contact', fields: { contact_email: "eviltrout@example.com" } - }, format: :json - expect(response).to be_forbidden - end - - it "updates properly if you are staff" do - put :update, params: { - id: 'contact', fields: { contact_email: "eviltrout@example.com" } - }, format: :json - - expect(response).to be_success - expect(SiteSetting.contact_email).to eq("eviltrout@example.com") - end - - it "returns errors if the field has them" do - put :update, params: { - id: 'contact', fields: { contact_email: "not-an-email" } - }, format: :json - - expect(response).to_not be_success - end - end - -end diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb deleted file mode 100644 index 62c8c55d5a..0000000000 --- a/spec/controllers/tags_controller_spec.rb +++ /dev/null @@ -1,219 +0,0 @@ -require 'rails_helper' - -describe TagsController do - describe 'show_latest' do - let(:tag) { Fabricate(:tag) } - let(:other_tag) { Fabricate(:tag) } - let(:third_tag) { Fabricate(:tag) } - let(:category) { Fabricate(:category) } - let(:subcategory) { Fabricate(:category, parent_category_id: category.id) } - - let(:single_tag_topic) { Fabricate(:topic, tags: [tag]) } - let(:multi_tag_topic) { Fabricate(:topic, tags: [tag, other_tag]) } - let(:all_tag_topic) { Fabricate(:topic, tags: [tag, other_tag, third_tag]) } - - context 'tagging disabled' do - it "returns 404" do - get :show_latest, params: { tag_id: tag.name }, format: :json - expect(response.status).to eq(404) - end - end - - context 'tagging enabled' do - before do - SiteSetting.tagging_enabled = true - end - - it "can filter by tag" do - get :show_latest, params: { tag_id: tag.name }, format: :json - expect(response).to be_success - end - - it "can filter by two tags" do - single_tag_topic; multi_tag_topic; all_tag_topic - - get :show_latest, params: { - tag_id: tag.name, additional_tag_ids: other_tag.name - }, format: :json - - expect(response).to be_success - - topic_ids = JSON.parse(response.body)["topic_list"]["topics"] - .map { |topic| topic["id"] } - - expect(topic_ids).to include(all_tag_topic.id) - expect(topic_ids).to include(multi_tag_topic.id) - expect(topic_ids).to_not include(single_tag_topic.id) - end - - it "can filter by multiple tags" do - single_tag_topic; multi_tag_topic; all_tag_topic - - get :show_latest, params: { - tag_id: tag.name, additional_tag_ids: "#{other_tag.name}/#{third_tag.name}" - }, format: :json - - expect(response).to be_success - - topic_ids = JSON.parse(response.body)["topic_list"]["topics"] - .map { |topic| topic["id"] } - - expect(topic_ids).to include(all_tag_topic.id) - expect(topic_ids).to_not include(multi_tag_topic.id) - expect(topic_ids).to_not include(single_tag_topic.id) - end - - it "does not find any tags when a tag which doesn't exist is passed" do - single_tag_topic - - get :show_latest, params: { - tag_id: tag.name, additional_tag_ids: "notatag" - }, format: :json - - expect(response).to be_success - - topic_ids = JSON.parse(response.body)["topic_list"]["topics"] - .map { |topic| topic["id"] } - - expect(topic_ids).to_not include(single_tag_topic.id) - end - - it "can filter by category and tag" do - get :show_latest, params: { - tag_id: tag.name, category: category.slug - }, format: :json - - expect(response).to be_success - end - - it "can filter by category, sub-category, and tag" do - get :show_latest, params: { - tag_id: tag.name, category: subcategory.slug, parent_category: category.slug - }, format: :json - - expect(response).to be_success - end - - it "can filter by category, no sub-category, and tag" do - get :show_latest, params: { - tag_id: tag.name, category: 'none', parent_category: category.slug - }, format: :json - - expect(response).to be_success - end - - it "can handle subcategories with the same name" do - category2 = Fabricate(:category) - subcategory2 = Fabricate(:category, - parent_category_id: category2.id, - name: subcategory.name, - slug: subcategory.slug - ) - t = Fabricate(:topic, category_id: subcategory2.id, tags: [other_tag]) - get :show_latest, params: { - tag_id: other_tag.name, category: subcategory2.slug, parent_category: category2.slug - }, format: :json - - expect(response).to be_success - - topic_ids = JSON.parse(response.body)["topic_list"]["topics"] - .map { |topic| topic["id"] } - - expect(topic_ids).to include(t.id) - end - - it "can filter by bookmarked" do - log_in(:user) - get :show_bookmarks, params: { - tag_id: tag.name - }, format: :json - - expect(response).to be_success - end - end - end - - describe 'search' do - context 'tagging disabled' do - it "returns 404" do - get :search, params: { q: 'stuff' }, format: :json - expect(response.status).to eq(404) - end - end - - context 'tagging enabled' do - before do - SiteSetting.tagging_enabled = true - end - - it "can return some tags" do - tag_names = ['stuff', 'stinky', 'stumped'] - tag_names.each { |name| Fabricate(:tag, name: name) } - get :search, params: { q: 'stu' }, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json["results"].map { |j| j["id"] }.sort).to eq(['stuff', 'stumped']) - end - - it "can say if given tag is not allowed" do - yup, nope = Fabricate(:tag, name: 'yup'), Fabricate(:tag, name: 'nope') - category = Fabricate(:category, tags: [yup]) - get :search, params: { q: 'nope', categoryId: category.id }, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json["results"].map { |j| j["id"] }.sort).to eq([]) - expect(json["forbidden"]).to be_present - end - - it "can return tags that are in secured categories but are allowed to be used" do - c = Fabricate(:private_category, group: Fabricate(:group)) - Fabricate(:topic, category: c, tags: [Fabricate(:tag, name: "cooltag")]) - get :search, params: { q: "cool" }, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json["results"].map { |j| j["id"] }).to eq(['cooltag']) - end - - it "supports Chinese and Russian" do - tag_names = ['房地产', 'тема-в-разработке'] - tag_names.each { |name| Fabricate(:tag, name: name) } - - get :search, params: { q: '房' }, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json["results"].map { |j| j["id"] }).to eq(['房地产']) - - get :search, params: { q: 'тема' }, format: :json - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json["results"].map { |j| j["id"] }).to eq(['тема-в-разработке']) - end - end - end - - describe 'destroy' do - context 'tagging enabled' do - before do - log_in(:admin) - SiteSetting.tagging_enabled = true - end - - context 'with an existent tag name' do - it 'deletes the tag' do - tag = Fabricate(:tag) - delete :destroy, params: { tag_id: tag.name }, format: :json - expect(response).to be_success - end - end - - context 'with a nonexistent tag name' do - it 'returns a tag not found message' do - delete :destroy, params: { tag_id: 'idontexist' }, format: :json - expect(response).not_to be_success - json = ::JSON.parse(response.body) - expect(json['error_type']).to eq('not_found') - end - end - end - end -end diff --git a/spec/controllers/topic_controller_spec.rb b/spec/controllers/topic_controller_spec.rb deleted file mode 100644 index 16ecfc9e33..0000000000 --- a/spec/controllers/topic_controller_spec.rb +++ /dev/null @@ -1,368 +0,0 @@ -require 'rails_helper' - -describe TopicsController do - before do - TopicUser.stubs(:track_visit!) - end - - let :topic do - Fabricate(:post).topic - end - - def set_referer(ref) - request.env['HTTP_REFERER'] = ref - end - - def set_accept_language(locale) - request.env['HTTP_ACCEPT_LANGUAGE'] = locale - end - - describe "themes" do - let :theme do - Theme.create!(user_id: -1, name: 'bob', user_selectable: true) - end - - let :theme2 do - Theme.create!(user_id: -1, name: 'bobbob', user_selectable: true) - end - - it "selects the theme the user has selected" do - user = log_in - user.user_option.update_columns(theme_key: theme.key) - - get :show, params: { id: 666 } - expect(controller.theme_key).to eq(theme.key) - - theme.update_attribute(:user_selectable, false) - - get :show, params: { id: 666 } - expect(controller.theme_key).not_to eq(theme.key) - end - - it "can be overridden with a cookie" do - user = log_in - user.user_option.update_columns(theme_key: theme.key) - - cookies['theme_key'] = "#{theme2.key},#{user.user_option.theme_key_seq}" - - get :show, params: { id: 666 } - expect(controller.theme_key).to eq(theme2.key) - - end - - it "cookie can fail back to user if out of sync" do - user = log_in - user.user_option.update_columns(theme_key: theme.key) - cookies['theme_key'] = "#{theme2.key},#{user.user_option.theme_key_seq - 1}" - - get :show, params: { id: 666 } - expect(controller.theme_key).to eq(theme.key) - end - end - - it "doesn't store an incoming link when there's no referer" do - expect { - get :show, params: { id: topic.id }, format: :json - }.not_to change(IncomingLink, :count) - end - - it "doesn't raise an error on a very long link" do - set_referer("http://#{'a' * 2000}.com") - - expect do - get :show, params: { id: topic.id }, format: :json - end.not_to raise_error - end - - describe "has_escaped_fragment?" do - render_views - - context "when the SiteSetting is disabled" do - - it "uses the application layout even with an escaped fragment param" do - SiteSetting.enable_escaped_fragments = false - - get :show, params: { - 'topic_id' => topic.id, - 'slug' => topic.slug, - '_escaped_fragment_' => 'true' - } - - body = response.body - - expect(body).to have_tag(:script, with: { src: '/assets/application.js' }) - expect(body).to_not have_tag(:meta, with: { name: 'fragment' }) - end - - end - - context "when the SiteSetting is enabled" do - before do - SiteSetting.enable_escaped_fragments = true - end - - it "uses the application layout when there's no param" do - get :show, params: { topic_id: topic.id, slug: topic.slug } - - body = response.body - - expect(body).to have_tag(:script, with: { src: '/assets/application.js' }) - expect(body).to have_tag(:meta, with: { name: 'fragment' }) - end - - it "uses the crawler layout when there's an _escaped_fragment_ param" do - get :show, params: { - topic_id: topic.id, - slug: topic.slug, - _escaped_fragment_: 'true' - } - - body = response.body - - expect(body).to have_tag(:body, with: { class: 'crawler' }) - expect(body).to_not have_tag(:meta, with: { name: 'fragment' }) - end - end - end - - describe "print" do - render_views - - context "when the SiteSetting is enabled" do - it "uses the application layout when there's no param" do - get :show, params: { topic_id: topic.id, slug: topic.slug } - - body = response.body - - expect(body).to have_tag(:script, src: '/assets/application.js') - expect(body).to have_tag(:meta, with: { name: 'fragment' }) - end - - it "uses the crawler layout when there's an print param" do - get :show, params: { topic_id: topic.id, slug: topic.slug, print: 'true' } - - body = response.body - - expect(body).to have_tag(:body, class: 'crawler') - expect(body).to_not have_tag(:meta, with: { name: 'fragment' }) - end - end - end - - describe 'clear_notifications' do - it 'correctly clears notifications if specified via cookie' do - notification = Fabricate(:notification) - log_in_user(notification.user) - - request.cookies['cn'] = "2828,100,#{notification.id}" - - get :show, params: { topic_id: 100, format: :json } - - expect(response.cookies['cn']).to eq nil - - notification.reload - expect(notification.read).to eq true - - end - - it 'correctly clears notifications if specified via header' do - notification = Fabricate(:notification) - log_in_user(notification.user) - - request.headers['Discourse-Clear-Notifications'] = "2828,100,#{notification.id}" - - get :show, params: { topic_id: 100, format: :json } - - notification.reload - expect(notification.read).to eq true - end - end - - describe "set_locale" do - context "allow_user_locale disabled" do - context "accept-language header differs from default locale" do - before do - SiteSetting.allow_user_locale = false - SiteSetting.default_locale = "en" - set_accept_language("fr") - end - - context "with an anonymous user" do - it "uses the default locale" do - get :show, params: { topic_id: topic.id, format: :json } - - expect(I18n.locale).to eq(:en) - end - end - - context "with a logged in user" do - it "it uses the default locale" do - user = Fabricate(:user, locale: :fr) - log_in_user(user) - - get :show, params: { topic_id: topic.id, format: :json } - - expect(I18n.locale).to eq(:en) - end - end - end - end - - context "set_locale_from_accept_language_header enabled" do - context "accept-language header differs from default locale" do - before do - SiteSetting.allow_user_locale = true - SiteSetting.set_locale_from_accept_language_header = true - SiteSetting.default_locale = "en" - set_accept_language("fr") - end - - context "with an anonymous user" do - it "uses the locale from the headers" do - get :show, params: { topic_id: topic.id, format: :json } - - expect(I18n.locale).to eq(:fr) - end - end - - context "with a logged in user" do - it "uses the user's preferred locale" do - user = Fabricate(:user, locale: :fr) - log_in_user(user) - - get :show, params: { topic_id: topic.id, format: :json } - - expect(I18n.locale).to eq(:fr) - end - end - end - - context "the preferred locale includes a region" do - it "returns the locale and region separated by an underscore" do - SiteSetting.allow_user_locale = true - SiteSetting.set_locale_from_accept_language_header = true - SiteSetting.default_locale = "en" - set_accept_language("zh-CN") - - get :show, params: { topic_id: topic.id, format: :json } - - expect(I18n.locale).to eq(:zh_CN) - end - end - - context 'accept-language header is not set' do - it 'uses the site default locale' do - SiteSetting.allow_user_locale = true - SiteSetting.default_locale = 'en' - set_accept_language('') - - get :show, params: { topic_id: topic.id, format: :json } - - expect(I18n.locale).to eq(:en) - end - end - end - end - - describe "read only header" do - it "returns no read only header by default" do - get :show, params: { topic_id: topic.id, format: :json } - expect(response.headers['Discourse-Readonly']).to eq(nil) - end - - it "returns a readonly header if the site is read only" do - Discourse.received_readonly! - get :show, params: { topic_id: topic.id, format: :json } - expect(response.headers['Discourse-Readonly']).to eq('true') - end - end -end - -describe 'api' do - - before do - ActionController::Base.allow_forgery_protection = true - end - - after do - ActionController::Base.allow_forgery_protection = false - end - - describe PostsController do - let(:user) do - Fabricate(:user) - end - - let(:post) do - Fabricate(:post) - end - - let(:api_key) { user.generate_api_key(user) } - let(:master_key) { ApiKey.create_master_key } - - # choosing an arbitrarily easy to mock trusted activity - it 'allows users with api key to bookmark posts' do - PostAction.expects(:act).with(user, post, PostActionType.types[:bookmark]).once - - put :bookmark, params: { - bookmarked: "true", - post_id: post.id, - api_key: api_key.key - }, format: :json - - expect(response).to be_success - end - - it 'raises an error with a user key that does not match an optionally specified username' do - PostAction.expects(:act).with(user, post, PostActionType.types[:bookmark]).never - - put :bookmark, params: { - bookmarked: "true", - post_id: post.id, - api_key: api_key.key, - api_username: 'made_up' - }, format: :json - - expect(response).not_to be_success - end - - it 'allows users with a master api key to bookmark posts' do - PostAction.expects(:act).with(user, post, PostActionType.types[:bookmark]).once - - put :bookmark, params: { - bookmarked: "true", - post_id: post.id, - api_key: master_key.key, - api_username: user.username - }, format: :json - - expect(response).to be_success - end - - it 'disallows phonies to bookmark posts' do - PostAction.expects(:act).with(user, post, PostActionType.types[:bookmark]).never - - put :bookmark, params: { - bookmarked: "true", - post_id: post.id, - api_key: SecureRandom.hex(32), - api_username: user.username - }, format: :json - - expect(response.code.to_i).to eq(403) - end - - it 'disallows blank api' do - PostAction.expects(:act).with(user, post, PostActionType.types[:bookmark]).never - - put :bookmark, params: { - bookmarked: "true", - post_id: post.id, - api_key: "", - api_username: user.username - }, format: :json - - expect(response.code.to_i).to eq(403) - end - end -end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb deleted file mode 100644 index a98cd9ebe7..0000000000 --- a/spec/controllers/uploads_controller_spec.rb +++ /dev/null @@ -1,250 +0,0 @@ -require 'rails_helper' - -describe UploadsController do - - context '.create' do - - it 'requires you to be logged in' do - post :create, format: :json - expect(response.status).to eq(403) - end - - context 'logged in' do - - before { @user = log_in :user } - - let(:logo) do - Rack::Test::UploadedFile.new(file_from_fixtures("logo.png")) - end - - let(:fake_jpg) do - Rack::Test::UploadedFile.new(file_from_fixtures("fake.jpg")) - end - - let(:text_file) do - Rack::Test::UploadedFile.new(File.new("#{Rails.root}/LICENSE.txt")) - end - - it 'expects a type' do - expect do - post :create, params: { format: :json, file: logo } - end.to raise_error(ActionController::ParameterMissing) - end - - it 'can look up long urls' do - upload = Fabricate(:upload) - post :lookup_urls, params: { short_urls: [upload.short_url], format: :json } - result = JSON.parse(response.body) - expect(result[0]["url"]).to eq(upload.url) - end - - it 'is successful with an image' do - Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything) - - post :create, params: { file: logo, type: "avatar", format: :json } - - expect(response.status).to eq 200 - expect(JSON.parse(response.body)["id"]).to be - end - - it 'is successful with an attachment' do - SiteSetting.authorized_extensions = "*" - - Jobs.expects(:enqueue).never - - post :create, params: { file: text_file, type: "composer", format: :json } - - expect(response.status).to eq 200 - id = JSON.parse(response.body)["id"] - expect(id).to be - end - - it 'is successful with api' do - SiteSetting.authorized_extensions = "*" - controller.stubs(:is_api?).returns(true) - - FinalDestination.stubs(:lookup_ip).returns("1.2.3.4") - - Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything) - - url = "http://example.com/image.png" - png = File.read(Rails.root + "spec/fixtures/images/logo.png") - - stub_request(:get, url).to_return(status: 200, body: png) - - post :create, params: { url: url, type: "avatar", format: :json } - - json = ::JSON.parse(response.body) - - expect(response.status).to eq 200 - expect(json["id"]).to be - expect(json["short_url"]).to eq("upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png") - end - - it 'correctly sets retain_hours for admins' do - log_in :admin - Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never - - post :create, params: { - file: logo, - retain_hours: 100, - type: "profile_background", - format: :json - } - - id = JSON.parse(response.body)["id"] - expect(Upload.find(id).retain_hours).to eq(100) - end - - it 'requires a file' do - Jobs.expects(:enqueue).never - - post :create, params: { type: "composer", format: :json } - - message = JSON.parse(response.body) - expect(response.status).to eq 422 - expect(message["errors"]).to contain_exactly(I18n.t("upload.file_missing")) - end - - it 'properly returns errors' do - SiteSetting.max_attachment_size_kb = 1 - - Jobs.expects(:enqueue).never - - post :create, params: { file: text_file, type: "avatar", format: :json } - - expect(response.status).to eq 422 - errors = JSON.parse(response.body)["errors"] - expect(errors).to be - end - - it 'ensures allow_uploaded_avatars is enabled when uploading an avatar' do - SiteSetting.allow_uploaded_avatars = false - post :create, params: { file: logo, type: "avatar", format: :json } - expect(response).to_not be_success - end - - it 'ensures sso_overrides_avatar is not enabled when uploading an avatar' do - SiteSetting.sso_overrides_avatar = true - post :create, params: { file: logo, type: "avatar", format: :json } - expect(response).to_not be_success - end - - it 'allows staff to upload any file in PM' do - SiteSetting.authorized_extensions = "jpg" - SiteSetting.allow_staff_to_upload_any_file_in_pm = true - @user.update_columns(moderator: true) - - post :create, params: { - file: text_file, - type: "composer", - for_private_message: "true", - format: :json - } - - expect(response).to be_success - id = JSON.parse(response.body)["id"] - expect(id).to be - end - - it 'respects `authorized_extensions_for_staff` setting when staff upload file' do - SiteSetting.authorized_extensions = "" - SiteSetting.authorized_extensions_for_staff = "*" - @user.update_columns(moderator: true) - - post :create, params: { - file: text_file, - type: "composer", - format: :json - } - - expect(response).to be_success - data = JSON.parse(response.body) - expect(data["id"]).to be - end - - it 'ignores `authorized_extensions_for_staff` setting when non-staff upload file' do - SiteSetting.authorized_extensions = "" - SiteSetting.authorized_extensions_for_staff = "*" - - post :create, params: { - file: text_file, - type: "composer", - format: :json - } - - data = JSON.parse(response.body) - expect(data["errors"].first).to eq(I18n.t("upload.unauthorized", authorized_extensions: '')) - end - - it 'returns an error when it could not determine the dimensions of an image' do - Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never - - post :create, params: { file: fake_jpg, type: "composer", format: :json } - - expect(response.status).to eq 422 - message = JSON.parse(response.body)["errors"] - expect(message).to contain_exactly(I18n.t("upload.images.size_not_found")) - end - - end - - end - - context '.show' do - - let(:site) { "default" } - let(:sha) { Digest::SHA1.hexdigest("discourse") } - - it "returns 404 when using external storage" do - store = stub(internal?: false) - Discourse.stubs(:store).returns(store) - Upload.expects(:find_by).never - - get :show, params: { site: site, sha: sha, extension: "pdf" } - expect(response.response_code).to eq(404) - end - - it "returns 404 when the upload doesn't exist" do - Upload.stubs(:find_by).returns(nil) - - get :show, params: { site: site, sha: sha, extension: "pdf" } - expect(response.response_code).to eq(404) - end - - it 'uses send_file' do - upload = build(:upload) - Upload.expects(:find_by).with(sha1: sha).returns(upload) - - controller.stubs(:render) - controller.expects(:send_file) - - get :show, params: { site: site, sha: sha, extension: "zip" } - end - - it "handles file without extension" do - SiteSetting.authorized_extensions = "*" - Fabricate(:upload, original_filename: "image_file", sha1: sha) - controller.stubs(:render) - controller.expects(:send_file) - - get :show, params: { site: site, sha: sha, format: :json } - expect(response).to be_success - end - - context "prevent anons from downloading files" do - - before { SiteSetting.prevent_anons_from_downloading_files = true } - - it "returns 404 when an anonymous user tries to download a file" do - Upload.expects(:find_by).never - - get :show, params: { site: site, sha: sha, extension: "pdf", format: :json } - expect(response.response_code).to eq(404) - end - - end - - end - -end diff --git a/spec/fabricators/topic_fabricator.rb b/spec/fabricators/topic_fabricator.rb index b3efbb98e3..4e276237a7 100644 --- a/spec/fabricators/topic_fabricator.rb +++ b/spec/fabricators/topic_fabricator.rb @@ -1,7 +1,9 @@ Fabricator(:topic) do user title { sequence(:title) { |i| "This is a test topic #{i}" } } - category_id { SiteSetting.uncategorized_category_id } + category_id do |attrs| + attrs[:category] ? attrs[:category].id : SiteSetting.uncategorized_category_id + end end Fabricator(:deleted_topic, from: :topic) do diff --git a/spec/fixtures/images/image_no_extension b/spec/fixtures/images/image_no_extension new file mode 100644 index 0000000000..5c900b61f7 Binary files /dev/null and b/spec/fixtures/images/image_no_extension differ diff --git a/spec/integrity/onceoff_integrity_spec.rb b/spec/integrity/onceoff_integrity_spec.rb new file mode 100644 index 0000000000..8143fe7946 --- /dev/null +++ b/spec/integrity/onceoff_integrity_spec.rb @@ -0,0 +1,14 @@ +require "rails_helper" + +describe Jobs::Onceoff do + it "can run all once off jobs without errors" do + # load all once offs + + Dir[Rails.root + 'app/jobs/onceoff/*.rb'].each do |f| + require_relative '../../app/jobs/onceoff/' + File.basename(f) + end + ObjectSpace.each_object(Class).select { |klass| klass < Jobs::Onceoff }.each do |j| + j.new.execute_onceoff(nil) + end + end +end diff --git a/spec/jobs/clean_up_exports_spec.rb b/spec/jobs/clean_up_exports_spec.rb deleted file mode 100644 index 741ac87a30..0000000000 --- a/spec/jobs/clean_up_exports_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -require 'rails_helper' - -require_dependency 'jobs/scheduled/clean_up_exports' - -describe Jobs::CleanUpExports do - it "runs correctly without crashing" do - Jobs::CleanUpExports.new.execute(nil) - end -end diff --git a/spec/jobs/clean_up_uploads_spec.rb b/spec/jobs/clean_up_uploads_spec.rb index 6539655807..b042405821 100644 --- a/spec/jobs/clean_up_uploads_spec.rb +++ b/spec/jobs/clean_up_uploads_spec.rb @@ -4,23 +4,44 @@ require_dependency 'jobs/scheduled/clean_up_uploads' describe Jobs::CleanUpUploads do - def fabricate_upload - Fabricate(:upload, created_at: 2.hours.ago) + def fabricate_upload(attributes = {}) + Fabricate(:upload, { created_at: 2.hours.ago }.merge(attributes)) end + let(:upload) { fabricate_upload } + before do - Upload.destroy_all SiteSetting.clean_up_uploads = true SiteSetting.clean_orphan_uploads_grace_period_hours = 1 @upload = fabricate_upload end it "deletes orphan uploads" do - expect(Upload.count).to be(1) + expect do + Jobs::CleanUpUploads.new.execute(nil) + end.to change { Upload.count }.by(-1) - Jobs::CleanUpUploads.new.execute(nil) + expect(Upload.exists?(id: @upload.id)).to eq(false) + end - expect(Upload.count).to be(0) + describe 'when clean_up_uploads is disabled' do + before do + SiteSetting.clean_up_uploads = false + end + + it 'should still delete invalid upload records' do + upload2 = fabricate_upload( + url: "", + retain_hours: nil + ) + + expect do + Jobs::CleanUpUploads.new.execute(nil) + end.to change { Upload.count }.by(-1) + + expect(Upload.exists?(id: @upload.id)).to eq(true) + expect(Upload.exists?(id: upload2.id)).to eq(false) + end end it "does not clean up uploads in site settings" do @@ -29,8 +50,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: logo_upload.id)).to eq(logo_upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: logo_upload.id)).to eq(true) end it "does not clean up uploads in site settings when they use the CDN" do @@ -41,8 +62,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: logo_small_upload.id)).to eq(logo_small_upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: logo_small_upload.id)).to eq(true) end it "does not delete profile background uploads" do @@ -51,8 +72,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: profile_background_upload.id)).to eq(profile_background_upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: profile_background_upload.id)).to eq(true) end it "does not delete card background uploads" do @@ -61,8 +82,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: card_background_upload.id)).to eq(card_background_upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: card_background_upload.id)).to eq(true) end it "does not delete category logo uploads" do @@ -71,8 +92,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: category_logo_upload.id)).to eq(category_logo_upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: category_logo_upload.id)).to eq(true) end it "does not delete category background url uploads" do @@ -81,8 +102,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: category_logo_upload.id)).to eq(category_logo_upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: category_logo_upload.id)).to eq(true) end it "does not delete post uploads" do @@ -91,8 +112,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: upload.id)).to eq(upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: upload.id)).to eq(true) end it "does not delete user uploaded avatar" do @@ -101,8 +122,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: upload.id)).to eq(upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: upload.id)).to eq(true) end it "does not delete user gravatar" do @@ -111,8 +132,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: upload.id)).to eq(upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: upload.id)).to eq(true) end it "does not delete user custom upload" do @@ -121,8 +142,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: upload.id)).to eq(upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: upload.id)).to eq(true) end it "does not delete uploads in a queued post" do @@ -139,9 +160,9 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: upload.id)).to eq(upload) - expect(Upload.find_by(id: upload2.id)).to eq(upload2) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: upload.id)).to eq(true) + expect(Upload.exists?(id: upload2.id)).to eq(true) end it "does not delete uploads in a draft" do @@ -152,9 +173,9 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: upload.id)).to eq(upload) - expect(Upload.find_by(id: upload2.id)).to eq(upload2) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: upload.id)).to eq(true) + expect(Upload.exists?(id: upload2.id)).to eq(true) end it "does not delete custom emojis" do @@ -163,8 +184,8 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: upload.id)).to eq(upload) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: upload.id)).to eq(true) end it "does not delete user exported csv uploads" do @@ -173,7 +194,7 @@ describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) - expect(Upload.find_by(id: @upload.id)).to eq(nil) - expect(Upload.find_by(id: csv_file.id)).to eq(csv_file) + expect(Upload.exists?(id: @upload.id)).to eq(false) + expect(Upload.exists?(id: csv_file.id)).to eq(true) end end diff --git a/spec/jobs/jobs_spec.rb b/spec/jobs/jobs_spec.rb index 9f49d3d5b6..01cbe6a225 100644 --- a/spec/jobs/jobs_spec.rb +++ b/spec/jobs/jobs_spec.rb @@ -77,6 +77,7 @@ describe Jobs do it 'deletes the matching job' do Sidekiq::Testing.disable! do + scheduled_jobs.clear expect(scheduled_jobs.size).to eq(0) Jobs.enqueue_in(1.year, :run_heartbeat, topic_id: 123) diff --git a/spec/jobs/toggle_topic_closed_spec.rb b/spec/jobs/toggle_topic_closed_spec.rb index 77e1afbe08..24b9d3c307 100644 --- a/spec/jobs/toggle_topic_closed_spec.rb +++ b/spec/jobs/toggle_topic_closed_spec.rb @@ -10,7 +10,7 @@ describe Jobs::ToggleTopicClosed do it 'should be able to close a topic' do topic - freeze_time(1.hour.from_now) do + freeze_time(61.minutes.from_now) do described_class.new.execute( topic_timer_id: topic.public_topic_timer.id, state: true @@ -19,7 +19,7 @@ describe Jobs::ToggleTopicClosed do expect(topic.reload.closed).to eq(true) expect(Post.last.raw).to eq(I18n.t( - 'topic_statuses.autoclosed_enabled_minutes', count: 60 + 'topic_statuses.autoclosed_enabled_minutes', count: 61 )) end end @@ -28,7 +28,7 @@ describe Jobs::ToggleTopicClosed do it 'should be work' do topic.update!(closed: true) - freeze_time(1.hour.from_now) do + freeze_time(61.minutes.from_now) do described_class.new.execute( topic_timer_id: topic.public_topic_timer.id, state: false @@ -37,7 +37,7 @@ describe Jobs::ToggleTopicClosed do expect(topic.reload.closed).to eq(false) expect(Post.last.raw).to eq(I18n.t( - 'topic_statuses.autoclosed_disabled_minutes', count: 60 + 'topic_statuses.autoclosed_disabled_minutes', count: 61 )) end end @@ -53,7 +53,7 @@ describe Jobs::ToggleTopicClosed do user: admin ) - freeze_time(1.hour.from_now) do + freeze_time(61.minutes.from_now) do described_class.new.execute( topic_timer_id: topic.public_topic_timer.id, state: false diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index bb8118d499..53ca30b5b5 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -440,7 +440,7 @@ describe UserNotifications do notification_data_hash: notification.data_hash ) - expect(mail.body).to include("#{I18n.t("user_notifications.pm_participants")} [group1 (2)](http://test.localhost/groups/group1), [group2 (1)](http://test.localhost/groups/group2), [one](http://test.localhost/u/one), [two](http://test.localhost/u/two)") + expect(mail.body).to include("[group1 (2)](http://test.localhost/groups/group1), [group2 (1)](http://test.localhost/groups/group2), [one](http://test.localhost/u/one), [two](http://test.localhost/u/two)") end context "when SiteSetting.group_name_in_subject is true" do diff --git a/spec/models/discourse_single_sign_on_spec.rb b/spec/models/discourse_single_sign_on_spec.rb index 8d7ddf0a3e..2aa6e6d520 100644 --- a/spec/models/discourse_single_sign_on_spec.rb +++ b/spec/models/discourse_single_sign_on_spec.rb @@ -2,7 +2,7 @@ require "rails_helper" describe DiscourseSingleSignOn do before do - @sso_url = "http://somesite.com/discourse_sso" + @sso_url = "http://example.com/discourse_sso" @sso_secret = "shjkfdhsfkjh" SiteSetting.sso_url = @sso_url diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 4027250bef..30a3ba3f42 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -406,7 +406,7 @@ describe Group do user = Fabricate(:user) user.change_trust_level!(TrustLevel[2]) - Group.exec_sql("UPDATE groups SET user_count = 0 WHERE id = #{Group::AUTO_GROUPS[:trust_level_2]}") + DB.exec("UPDATE groups SET user_count = 0 WHERE id = #{Group::AUTO_GROUPS[:trust_level_2]}") Group.refresh_automatic_groups! diff --git a/spec/models/incoming_link_spec.rb b/spec/models/incoming_link_spec.rb index 00e3b9825c..c44fa14fcd 100644 --- a/spec/models/incoming_link_spec.rb +++ b/spec/models/incoming_link_spec.rb @@ -69,7 +69,7 @@ describe IncomingLink do end it "does nothing if referer is same as host" do - add(post_id: 1, host: 'somesite.com', referer: 'http://somesite.com') + add(post_id: 1, host: 'example.com', referer: 'http://example.com') expect(IncomingLink.count).to eq 0 end diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb index b460306d36..96fe9ea0d2 100644 --- a/spec/models/post_action_spec.rb +++ b/spec/models/post_action_spec.rb @@ -9,7 +9,7 @@ describe PostAction do let(:eviltrout) { Fabricate(:evil_trout) } let(:admin) { Fabricate(:admin) } let(:post) { Fabricate(:post) } - let(:second_post) { Fabricate(:post, topic_id: post.topic_id) } + let(:second_post) { Fabricate(:post, topic: post.topic) } let(:bookmark) { PostAction.new(user_id: post.user_id, post_action_type_id: PostActionType.types[:bookmark] , post_id: post.id) } def value_for(user_id, dt) diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 8a7d577684..9c89c807dd 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -452,6 +452,13 @@ describe Post do post_two_links.user.trust_level = TrustLevel[1] expect(post_one_link).not_to be_valid end + + it "will skip the check for whitelisted domains" do + SiteSetting.whitelisted_link_domains = 'www.bbc.co.uk' + SiteSetting.min_trust_to_post_links = 2 + post_two_links.user.trust_level = TrustLevel[1] + expect(post_one_link).to be_valid + end end end @@ -798,12 +805,13 @@ describe Post do let!(:p1) { Fabricate(:post, post_args.merge(score: 4, percent_rank: 0.33)) } let!(:p2) { Fabricate(:post, post_args.merge(score: 10, percent_rank: 0.66)) } let!(:p3) { Fabricate(:post, post_args.merge(score: 5, percent_rank: 0.99)) } + let!(:p4) { Fabricate(:post, percent_rank: 0.99) } it "returns the OP and posts above the threshold in summary mode" do SiteSetting.summary_percent_filter = 66 - expect(Post.summary.order(:post_number)).to eq([p1, p2]) + expect(Post.summary(topic.id).order(:post_number)).to eq([p1, p2]) + expect(Post.summary(p4.topic.id)).to eq([p4]) end - end context 'sort_order' do @@ -935,12 +943,12 @@ describe Post do end describe "has_host_spam" do - let(:raw) { "hello from my site http://www.somesite.com http://#{GlobalSetting.hostname} http://#{RailsMultisite::ConnectionManagement.current_hostname}" } + let(:raw) { "hello from my site http://www.example.net http://#{GlobalSetting.hostname} http://#{RailsMultisite::ConnectionManagement.current_hostname}" } it "correctly detects host spam" do post = Fabricate(:post, raw: raw) - expect(post.total_hosts_usage).to eq("www.somesite.com" => 1) + expect(post.total_hosts_usage).to eq("www.example.net" => 1) post.acting_user.trust_level = 0 expect(post.has_host_spam?).to eq(false) @@ -949,7 +957,7 @@ describe Post do expect(post.has_host_spam?).to eq(true) - SiteSetting.white_listed_spam_host_domains = "bla.com|boo.com | somesite.com " + SiteSetting.white_listed_spam_host_domains = "bla.com|boo.com | example.net " expect(post.has_host_spam?).to eq(false) end @@ -960,6 +968,26 @@ describe Post do expect(post.has_host_spam?).to eq(false) end + it "punishes previously staged users that were created within 1 day" do + SiteSetting.newuser_spam_host_threshold = 1 + SiteSetting.newuser_max_links = 3 + user = Fabricate(:user, staged: true, trust_level: 0) + user.created_at = 1.hour.ago + user.unstage + post = Fabricate(:post, raw: raw, user: user) + expect(post.has_host_spam?).to eq(true) + end + + it "doesn't punish previously staged users over 1 day old" do + SiteSetting.newuser_spam_host_threshold = 1 + SiteSetting.newuser_max_links = 3 + user = Fabricate(:user, staged: true, trust_level: 0) + user.created_at = 1.day.ago + user.unstage + post = Fabricate(:post, raw: raw, user: user) + expect(post.has_host_spam?).to eq(false) + end + it "ignores private messages" do SiteSetting.newuser_spam_host_threshold = 1 user = Fabricate(:user, trust_level: 0) @@ -987,7 +1015,7 @@ describe Post do first_baked = post.baked_at first_cooked = post.cooked - Post.exec_sql("UPDATE posts SET cooked = 'frogs' WHERE id = ?", post.id) + DB.exec("UPDATE posts SET cooked = 'frogs' WHERE id = ?", [ post.id ]) post.reload post.expects(:publish_change_to_clients!).with(:rebaked) diff --git a/spec/models/queued_post_spec.rb b/spec/models/queued_post_spec.rb index b6e5113efe..5616ef41a0 100644 --- a/spec/models/queued_post_spec.rb +++ b/spec/models/queued_post_spec.rb @@ -86,6 +86,11 @@ describe QueuedPost do # It removes the pending action expect(UserAction.where(queued_post_id: qp.id).count).to eq(0) + # Logs staff action for rejected post + post_rejected_logs = UserHistory.where(action: UserHistory.actions[:post_rejected]) + expect(post_rejected_logs.count).to eq(1) + expect(post_rejected_logs.first.details).to include(qp.raw) + # We can't reject twice expect(-> { qp.reject!(admin) }).to raise_error(QueuedPost::InvalidStateTransition) end diff --git a/spec/models/search_log_spec.rb b/spec/models/search_log_spec.rb index 8811fca9ab..af9069b16d 100644 --- a/spec/models/search_log_spec.rb +++ b/spec/models/search_log_spec.rb @@ -101,7 +101,7 @@ RSpec.describe SearchLog, type: :model do log = SearchLog.find(log_id) expect(log.term).to eq('hello') expect(log.search_type).to eq(SearchLog.search_types[:full_page]) - expect(log.ip_address).to eq('192.168.0.1') + expect(log.ip_address).to eq(nil) expect(log.user_id).to eq(user.id) action, updated_log_id = SearchLog.log( @@ -183,11 +183,12 @@ RSpec.describe SearchLog, type: :model do end context "trending" do + let(:user) { Fabricate(:user) } before do SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1') SearchLog.log(term: 'php', search_type: :header, ip_address: '127.0.0.1') SearchLog.log(term: 'java', search_type: :header, ip_address: '127.0.0.1') - SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1', user_id: Fabricate(:user).id) + SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.1', user_id: user.id) SearchLog.log(term: 'swift', search_type: :header, ip_address: '127.0.0.1') SearchLog.log(term: 'ruby', search_type: :header, ip_address: '127.0.0.2') end @@ -207,6 +208,7 @@ RSpec.describe SearchLog, type: :model do expect(top_trending.click_through).to eq(0) SearchLog.where(term: 'ruby', ip_address: '127.0.0.1').update_all(search_result_id: 12) + SearchLog.where(term: 'ruby', user_id: user.id).update_all(search_result_id: 12) SearchLog.where(term: 'ruby', ip_address: '127.0.0.2').update_all(search_result_id: 24) top_trending = SearchLog.trending.first expect(top_trending.click_through).to eq(3) diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index c7cc72496b..69dd546e8d 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -48,12 +48,12 @@ describe Tag do describe '#top_tags' do it "returns nothing if nothing has been tagged" do make_some_tags(tag_a_topic: false) - expect(described_class.top_tags.sort).to be_empty + expect(Tag.top_tags.sort).to be_empty end it "can return all tags" do make_some_tags(tag_a_topic: true) - expect(described_class.top_tags.sort).to eq(@tags.map(&:name).sort) + expect(Tag.top_tags.sort).to eq(@tags.map(&:name).sort) end context "with categories" do @@ -69,27 +69,17 @@ describe Tag do @topics << Fabricate(:topic, category: @private_category, tags: [@tags[2]]) end - it "doesn't return tags that have only been used in private category to anon" do - expect(described_class.top_tags.sort).to eq([@tags[0].name, @tags[1].name].sort) - end + it "works correctly" do + expect(Tag.top_tags(category: @category1).sort).to eq([@tags[0].name].sort) + expect(Tag.top_tags(guardian: Guardian.new(Fabricate(:admin))).sort).to eq([@tags[0].name, @tags[1].name, @tags[2].name].sort) + expect(Tag.top_tags(category: @private_category, guardian: Guardian.new(Fabricate(:admin))).sort).to eq([@tags[2].name].sort) - it "returns tags used in private category to those who can see that category" do - expect(described_class.top_tags(guardian: Guardian.new(Fabricate(:admin))).sort).to eq([@tags[0].name, @tags[1].name, @tags[2].name].sort) - end + expect(Tag.top_tags.sort).to eq([@tags[0].name, @tags[1].name].sort) + expect(Tag.top_tags(category: @private_category)).to be_empty - it "returns tags scoped to a given category" do - expect(described_class.top_tags(category: @category1).sort).to eq([@tags[0].name].sort) - expect(described_class.top_tags(category: @private_category, guardian: Guardian.new(Fabricate(:admin))).sort).to eq([@tags[2].name].sort) - end - - it "returns tags from sub-categories too" do sub_category = Fabricate(:category, parent_category_id: @category1.id) Fabricate(:topic, category: sub_category, tags: [@tags[1]]) - expect(described_class.top_tags(category: @category1).sort).to eq([@tags[0].name, @tags[1].name].sort) - end - - it "returns nothing if category arg is private to you" do - expect(described_class.top_tags(category: @private_category)).to be_empty + expect(Tag.top_tags(category: @category1).sort).to eq([@tags[0].name, @tags[1].name].sort) end end @@ -105,15 +95,15 @@ describe Tag do end it "for category with restricted tags, lists those tags" do - expect(described_class.top_tags(category: @category1)).to eq([@tags[0].name]) + expect(Tag.top_tags(category: @category1)).to eq([@tags[0].name]) end it "for category without tags, lists allowed tags" do - expect(described_class.top_tags(category: @category2).sort).to eq([@tags[1].name, @tags[2].name].sort) + expect(Tag.top_tags(category: @category2).sort).to eq([@tags[1].name, @tags[2].name].sort) end it "for no category arg, lists all tags" do - expect(described_class.top_tags.sort).to eq([@tags[0].name, @tags[1].name, @tags[2].name].sort) + expect(Tag.top_tags.sort).to eq([@tags[0].name, @tags[1].name, @tags[2].name].sort) end end @@ -151,17 +141,17 @@ describe Tag do end it "returns nothing if user is not a staff" do - expect(described_class.pm_tags(guardian: Guardian.new(regular_user))).to be_empty + expect(Tag.pm_tags(guardian: Guardian.new(regular_user))).to be_empty end it "returns nothing if allow_staff_to_tag_pms setting is disabled" do SiteSetting.allow_staff_to_tag_pms = false - expect(described_class.pm_tags(guardian: Guardian.new(admin)).sort).to be_empty + expect(Tag.pm_tags(guardian: Guardian.new(admin)).sort).to be_empty end it "returns all pm tags if user is a staff and pm tagging is enabled" do SiteSetting.allow_staff_to_tag_pms = true - tags = described_class.pm_tags(guardian: Guardian.new(admin), allowed_user: regular_user) + tags = Tag.pm_tags(guardian: Guardian.new(admin), allowed_user: regular_user) expect(tags.length).to eq(2) expect(tags.map { |t| t[:id] }).to contain_exactly("tag-0", "tag-1") end @@ -171,7 +161,7 @@ describe Tag do it "should exclude private message topics" do topic Fabricate(:private_message_topic, tags: [tag]) - described_class.ensure_consistency! + Tag.ensure_consistency! tag.reload expect(tag.topic_count).to eq(1) end diff --git a/spec/models/topic_featured_users_spec.rb b/spec/models/topic_featured_users_spec.rb index 09459115ea..b874854d6b 100644 --- a/spec/models/topic_featured_users_spec.rb +++ b/spec/models/topic_featured_users_spec.rb @@ -4,11 +4,11 @@ describe TopicFeaturedUsers do it 'ensures consistency' do t = Fabricate(:topic) - Fabricate(:post, topic_id: t.id, user_id: t.user_id) - p2 = Fabricate(:post, topic_id: t.id) - p3 = Fabricate(:post, topic_id: t.id, user_id: p2.user_id) - p4 = Fabricate(:post, topic_id: t.id) - p5 = Fabricate(:post, topic_id: t.id) + Fabricate(:post, topic: t, user: t.user) + p2 = Fabricate(:post, topic: t) + p3 = Fabricate(:post, topic: t, user: p2.user) + p4 = Fabricate(:post, topic: t) + p5 = Fabricate(:post, topic: t) t.update_columns(featured_user1_id: 66, featured_user2_id: 70, diff --git a/spec/models/topic_link_spec.rb b/spec/models/topic_link_spec.rb index 606ba5f7ba..d3d53a0728 100644 --- a/spec/models/topic_link_spec.rb +++ b/spec/models/topic_link_spec.rb @@ -330,7 +330,7 @@ http://b.com/#{'a' * 500} array = TopicLink.topic_map(Guardian.new, post.topic_id) expect(array.length).to eq(6) - expect(array[0]["clicks"]).to eq(1) + expect(array[0].clicks).to eq(1) end it 'secures internal links correctly' do diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 57ad210e34..0da4b29bd6 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1612,8 +1612,7 @@ describe Topic do it "doesn't return topics from TL0 users" do new_user = Fabricate(:user, trust_level: 0) - Fabricate(:topic, user_id: new_user.id) - + Fabricate(:topic, user: new_user) expect(Topic.for_digest(user, 1.year.ago, top_order: true)).to be_blank end @@ -1626,7 +1625,7 @@ describe Topic do it "returns topics from TL0 users if enabled in preferences" do new_user = Fabricate(:user, trust_level: 0) - topic = Fabricate(:topic, user_id: new_user.id) + topic = Fabricate(:topic, user: new_user) u = Fabricate(:user) u.user_option.include_tl0_in_digests = true @@ -1656,7 +1655,7 @@ describe Topic do user = Fabricate(:user) muted_tag = Fabricate(:tag) TagUser.change(user.id, muted_tag.id, TagUser.notification_levels[:muted]) - topic1 = Fabricate(:topic, tags: [muted_tag]) + _topic1 = Fabricate(:topic, tags: [muted_tag]) topic2 = Fabricate(:topic, tags: [Fabricate(:tag), Fabricate(:tag)]) topic3 = Fabricate(:topic) @@ -1693,7 +1692,7 @@ describe Topic do it "excludes topics that are within the grace period" do topic1 = Fabricate(:topic, created_at: 6.minutes.ago) - topic2 = Fabricate(:topic, created_at: 4.minutes.ago) + _topic2 = Fabricate(:topic, created_at: 4.minutes.ago) expect(Topic.for_digest(user, 1.year.ago, top_order: true)).to eq([topic1]) end end @@ -2180,9 +2179,9 @@ describe Topic do end it "returns 0 with a topic with 1 reply" do - topic = Fabricate(:topic, created_at: 5.hours.ago) - post1 = Fabricate(:post, topic: topic, user: topic.user, post_number: 1, created_at: 5.hours.ago) - post1 = Fabricate(:post, topic: topic, post_number: 2, created_at: 2.hours.ago) + topic = Fabricate(:topic, created_at: 5.hours.ago) + _post1 = Fabricate(:post, topic: topic, user: topic.user, post_number: 1, created_at: 5.hours.ago) + _post2 = Fabricate(:post, topic: topic, post_number: 2, created_at: 2.hours.ago) expect(Topic.with_no_response_per_day(5.days.ago, Time.zone.now).count).to eq(0) expect(Topic.with_no_response_total).to eq(0) end diff --git a/spec/models/topic_timer_spec.rb b/spec/models/topic_timer_spec.rb index 639ef1d55e..b702f9a77e 100644 --- a/spec/models/topic_timer_spec.rb +++ b/spec/models/topic_timer_spec.rb @@ -1,10 +1,23 @@ require 'rails_helper' RSpec.describe TopicTimer, type: :model do - let(:topic_timer) { Fabricate(:topic_timer) } + let(:topic_timer) { + # we should not need to do this but somehow + # fabricator is failing here + TopicTimer.create!( + user_id: -1, + topic: Fabricate(:topic), + execute_at: 1.hour.from_now, + status_type: TopicTimer.types[:close] + ) + } let(:topic) { Fabricate(:topic) } let(:admin) { Fabricate(:admin) } + before do + freeze_time Time.new(2018) + end + context "validations" do describe '#status_type' do it 'should ensure that only one active public topic status update exists' do diff --git a/spec/models/topic_user_spec.rb b/spec/models/topic_user_spec.rb index c0ed6c986b..ed9de27c3a 100644 --- a/spec/models/topic_user_spec.rb +++ b/spec/models/topic_user_spec.rb @@ -311,8 +311,10 @@ describe TopicUser do it 'should update tracking state when you reply' do new_user.user_option.update_column(:notification_level_when_replying, 3) post_creator.create - TopicUser.exec_sql("UPDATE topic_users set notification_level=2 - WHERE topic_id = :topic_id AND user_id = :user_id", topic_id: topic_new_user.topic_id, user_id: topic_new_user.user_id) + DB.exec("UPDATE topic_users set notification_level=2 + WHERE topic_id = :topic_id AND user_id = :user_id", + topic_id: topic_new_user.topic_id, user_id: topic_new_user.user_id) + TopicUser.auto_notification(topic_new_user.user_id, topic_new_user.topic_id, TopicUser.notification_reasons[:created_post], TopicUser.notification_levels[:watching]) tu = TopicUser.find_by(user_id: topic_new_user.user_id, topic_id: topic_new_user.topic_id) @@ -322,7 +324,7 @@ describe TopicUser do it 'should not update tracking state when you reply' do new_user.user_option.update_column(:notification_level_when_replying, 3) post_creator.create - TopicUser.exec_sql("UPDATE topic_users set notification_level=3 + DB.exec("UPDATE topic_users set notification_level=3 WHERE topic_id = :topic_id AND user_id = :user_id", topic_id: topic_new_user.topic_id, user_id: topic_new_user.user_id) TopicUser.auto_notification(topic_new_user.user_id, topic_new_user.topic_id, TopicUser.notification_reasons[:created_post], TopicUser.notification_levels[:tracking]) @@ -333,7 +335,7 @@ describe TopicUser do it 'should not update tracking state when state manually set to normal you reply' do new_user.user_option.update_column(:notification_level_when_replying, 3) post_creator.create - TopicUser.exec_sql("UPDATE topic_users set notification_level=1 + DB.exec("UPDATE topic_users set notification_level=1 WHERE topic_id = :topic_id AND user_id = :user_id", topic_id: topic_new_user.topic_id, user_id: topic_new_user.user_id) TopicUser.auto_notification(topic_new_user.user_id, topic_new_user.topic_id, TopicUser.notification_reasons[:created_post], TopicUser.notification_levels[:tracking]) @@ -344,7 +346,7 @@ describe TopicUser do it 'should not update tracking state when state manually set to muted you reply' do new_user.user_option.update_column(:notification_level_when_replying, 3) post_creator.create - TopicUser.exec_sql("UPDATE topic_users set notification_level=0 + DB.exec("UPDATE topic_users set notification_level=0 WHERE topic_id = :topic_id AND user_id = :user_id", topic_id: topic_new_user.topic_id, user_id: topic_new_user.user_id) TopicUser.auto_notification(topic_new_user.user_id, topic_new_user.topic_id, TopicUser.notification_reasons[:created_post], TopicUser.notification_levels[:tracking]) @@ -417,7 +419,7 @@ describe TopicUser do p2 = Fabricate(:post, user: p1.user, topic: p1.topic, post_number: 2) p1.topic.notifier.watch_topic!(p1.user_id) - TopicUser.exec_sql("UPDATE topic_users set highest_seen_post_number=1, last_read_post_number=0 + DB.exec("UPDATE topic_users set highest_seen_post_number=1, last_read_post_number=0 WHERE topic_id = :topic_id AND user_id = :user_id", topic_id: p1.topic_id, user_id: p1.user_id) [p1, p2].each do |p| diff --git a/spec/models/topic_view_item_spec.rb b/spec/models/topic_view_item_spec.rb index d811a56c40..da0762d871 100644 --- a/spec/models/topic_view_item_spec.rb +++ b/spec/models/topic_view_item_spec.rb @@ -27,4 +27,14 @@ describe TopicViewItem do expect(user.user_stat.topics_entered).to eq(1) end + it "does not log IP address for logged-in users" do + topic = Fabricate(:topic) + user = Fabricate(:user) + add(topic.id, "1.1.1.1", user.id) + + expect(TopicViewItem.find_by(topic_id: topic.id, user_id: user.id).ip_address).to eq(nil) + add(topic.id, "1.2.3.4", nil) + expect(TopicViewItem.find_by(topic_id: topic.id, user_id: nil).ip_address).to eq("1.2.3.4") + end + end diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 32b86bbc18..697cd266ee 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -106,6 +106,13 @@ describe Upload do SiteSetting.enable_s3_uploads = false end + it "should return the right upload when using base url (not CDN) for s3" do + upload + url = "https://#{SiteSetting.s3_upload_bucket}.s3.amazonaws.com#{path}" + + expect(Upload.get_from_url(url)).to eq(upload) + end + it "should return the right upload when using a CDN for s3" do upload s3_cdn_url = 'https://mycdn.slowly.net' diff --git a/spec/models/user_action_spec.rb b/spec/models/user_action_spec.rb index 6ab8a1537a..9a172ded34 100644 --- a/spec/models/user_action_spec.rb +++ b/spec/models/user_action_spec.rb @@ -43,7 +43,7 @@ describe UserAction do end def stats_for_user(viewer = nil) - UserAction.stats(user.id, Guardian.new(viewer)).map { |r| r["action_type"].to_i }.sort + UserAction.stats(user.id, Guardian.new(viewer)).map { |r| r.action_type.to_i }.sort end def stream(viewer = nil) diff --git a/spec/models/user_export_spec.rb b/spec/models/user_export_spec.rb new file mode 100644 index 0000000000..f1d54324b1 --- /dev/null +++ b/spec/models/user_export_spec.rb @@ -0,0 +1,28 @@ +require 'rails_helper' + +RSpec.describe UserExport do + let(:user) { Fabricate(:user) } + + describe '.remove_old_exports' do + it 'should remove the right records' do + export = UserExport.create!( + file_name: "test", + user: user, + created_at: 3.days.ago + ) + + export2 = UserExport.create!( + file_name: "test2", + user: user, + created_at: 1.day.ago + ) + + expect do + UserExport.remove_old_exports + end.to change { UserExport.count }.by(-1) + + expect(UserExport.exists?(id: export.id)).to eq(false) + expect(UserExport.exists?(id: export2.id)).to eq(true) + end + end +end diff --git a/spec/models/user_profile_spec.rb b/spec/models/user_profile_spec.rb index ee05902c3e..98545ffabe 100644 --- a/spec/models/user_profile_spec.rb +++ b/spec/models/user_profile_spec.rb @@ -114,19 +114,19 @@ describe UserProfile do describe 'bio excerpt emojis' do let(:user) { Fabricate(:user) } + let(:upload) { Fabricate(:upload) } before do - CustomEmoji.create!(name: 'test', upload_id: 1) + CustomEmoji.create!(name: 'test', upload: upload) Emoji.clear_cache - user.user_profile.bio_raw = "hello :test: :woman_scientist:t5: 🤔" - user.user_profile.save - user.user_profile.reload + user.user_profile.update!( + bio_raw: "hello :test: :woman_scientist:t5: 🤔" + ) end it 'supports emoji images' do - - expect(user.user_profile.bio_excerpt(500, keep_emoji_images: true)).to eq("hello \":test:\" \":woman_scientist:t5:\" \":thinking:\"") + expect(user.user_profile.bio_excerpt(500, keep_emoji_images: true)).to eq("hello \":test:\" \":woman_scientist:t5:\" \":thinking:\"") end end diff --git a/spec/multisite/jobs_spec.rb b/spec/multisite/jobs_spec.rb index 076f6cea54..2434bda3d4 100644 --- a/spec/multisite/jobs_spec.rb +++ b/spec/multisite/jobs_spec.rb @@ -5,7 +5,6 @@ RSpec.describe "Running Sidekiq Jobs in Multisite" do before do conn.config_filename = "spec/fixtures/multisite/two_dbs.yml" - SiteSetting.defaults.refresh_site_locale! end after do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index f86eb06bc6..96495386b6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -131,6 +131,8 @@ RSpec.configure do |config| x.exception.cause = ex end end + + unfreeze_time end config.before :each do |x| @@ -153,15 +155,10 @@ RSpec.configure do |config| SiteSetting.provider.all.each do |setting| SiteSetting.remove_override!(setting.name) end - SiteSetting.defaults.site_locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE # very expensive IO operations SiteSetting.automatically_download_gravatars = false - # Running jobs are expensive and most of our tests are not concern with - # code that runs inside jobs - SiteSetting.queue_jobs = true - Discourse.clear_readonly! Sidekiq::Worker.clear_all diff --git a/spec/requests/about_controller_spec.rb b/spec/requests/about_controller_spec.rb index e4e4b01a0a..cf7ab39d5e 100644 --- a/spec/requests/about_controller_spec.rb +++ b/spec/requests/about_controller_spec.rb @@ -8,7 +8,7 @@ describe AboutController do SiteSetting.login_required = false get "/about" - expect(response).to be_success + expect(response.status).to eq(200) end it 'should redirect to login page for anonymous user when login_required is true' do @@ -23,7 +23,7 @@ describe AboutController do sign_in(Fabricate(:user)) get "/about" - expect(response).to be_success + expect(response.status).to eq(200) end end end diff --git a/spec/requests/admin/admin_controller_spec.rb b/spec/requests/admin/admin_controller_spec.rb index c479ada153..062f3a3a0e 100644 --- a/spec/requests/admin/admin_controller_spec.rb +++ b/spec/requests/admin/admin_controller_spec.rb @@ -1,11 +1,19 @@ require 'rails_helper' RSpec.describe Admin::AdminController do - it "should return the right response if user isn't a staff" do - get "/admin", params: { api_key: 'asdiasiduga' } - expect(response.status).to eq(404) + describe '#index' do + it "needs you to be logged in" do + get "/admin.json" + expect(response.status).to eq(404) + end - get "/admin" - expect(response.status).to eq(404) + it "should return the right response if user isn't a staff" do + sign_in(Fabricate(:user)) + get "/admin", params: { api_key: 'asdiasiduga' } + expect(response.status).to eq(404) + + get "/admin" + expect(response.status).to eq(404) + end end end diff --git a/spec/requests/admin/api_controller_spec.rb b/spec/requests/admin/api_controller_spec.rb new file mode 100644 index 0000000000..feee32728b --- /dev/null +++ b/spec/requests/admin/api_controller_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +describe Admin::ApiController do + + it "is a subclass of AdminController" do + expect(Admin::ApiController < Admin::AdminController).to eq(true) + end + + let(:admin) { Fabricate(:admin) } + before do + sign_in(admin) + end + + describe '#index' do + it "succeeds" do + get "/admin/api/keys.json" + expect(response.status).to eq(200) + end + end + + describe '#regenerate_key' do + let(:api_key) { Fabricate(:api_key) } + + it "returns 404 when there is no key" do + put "/admin/api/key.json", params: { id: 1234 } + expect(response.status).to eq(404) + end + + it "delegates to the api key's `regenerate!` method" do + prev_value = api_key.key + put "/admin/api/key.json", params: { id: api_key.id } + expect(response.status).to eq(200) + + api_key.reload + expect(api_key.key).not_to eq(prev_value) + expect(api_key.created_by.id).to eq(admin.id) + end + end + + describe '#revoke_key' do + let(:api_key) { Fabricate(:api_key) } + + it "returns 404 when there is no key" do + delete "/admin/api/key.json", params: { id: 1234 } + expect(response.status).to eq(404) + end + + it "delegates to the api key's `regenerate!` method" do + delete "/admin/api/key.json", params: { id: api_key.id } + expect(response.status).to eq(200) + expect(ApiKey.where(key: api_key.key).count).to eq(0) + end + end + + describe '#create_master_key' do + it "creates a record" do + expect do + post "/admin/api/key.json" + end.to change(ApiKey, :count).by(1) + expect(response.status).to eq(200) + end + end +end diff --git a/spec/requests/admin/backups_controller_spec.rb b/spec/requests/admin/backups_controller_spec.rb index 7504cb31c3..4224bcb07b 100644 --- a/spec/requests/admin/backups_controller_spec.rb +++ b/spec/requests/admin/backups_controller_spec.rb @@ -2,16 +2,254 @@ require 'rails_helper' RSpec.describe Admin::BackupsController do let(:admin) { Fabricate(:admin) } + let(:backup_filename) { "2014-02-10-065935.tar.gz" } + let(:backup_filename2) { "2014-02-11-065935.tar.gz" } + + it "is a subclass of AdminController" do + expect(Admin::BackupsController < Admin::AdminController).to eq(true) + end before do sign_in(admin) end + after do + $redis.flushall + end + describe "#index" do it "raises an error when backups are disabled" do SiteSetting.enable_backups = false get "/admin/backups.json" - expect(response).not_to be_success + expect(response.status).to eq(403) + end + + context "html format" do + it "preloads important data" do + get "/admin/backups.html" + expect(response.status).to eq(200) + + preloaded = controller.instance_variable_get("@preloaded").map do |key, value| + [key, JSON.parse(value)] + end.to_h + + expect(preloaded["backups"].size).to eq(Backup.all.size) + expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status) + expect(preloaded["logs"].size).to eq(BackupRestore.logs.size) + end + end + + context "json format" do + it "returns a list of all the backups" do + begin + paths = [] + [backup_filename, backup_filename2].each do |name| + path = File.join(Backup.base_directory, name) + paths << path + File.open(path, "w") { |f| f.write("hello") } + Backup.create_from_filename(name) + end + + get "/admin/backups.json" + + expect(response.status).to eq(200) + + json = JSON.parse(response.body).map { |backup| backup["filename"] } + expect(json).to include(backup_filename) + expect(json).to include(backup_filename2) + ensure + paths.each { |path| File.delete(path) } + end + end + end + end + + describe '#status' do + it "returns the current backups status" do + get "/admin/backups/status.json" + expect(response.body).to eq(BackupRestore.operations_status.to_json) + expect(response.status).to eq(200) + end + end + + describe '#create' do + it "starts a backup" do + BackupRestore.expects(:backup!).with(admin.id, publish_to_message_bus: true, with_uploads: false, client_id: "foo") + + post "/admin/backups.json", params: { + with_uploads: false, client_id: "foo" + } + + expect(response.status).to eq(200) + end + end + + describe '#show' do + it "uses send_file to transmit the backup" do + begin + token = EmailBackupToken.set(admin.id) + path = File.join(Backup.base_directory, backup_filename) + File.open(path, "w") { |f| f.write("hello") } + + Backup.create_from_filename(backup_filename) + + expect do + get "/admin/backups/#{backup_filename}.json", params: { token: token } + end.to change { UserHistory.where(action: UserHistory.actions[:backup_download]).count }.by(1) + + expect(response.headers['Content-Length']).to eq("5") + expect(response.headers['Content-Disposition']).to match(/attachment; filename/) + ensure + File.delete(path) + EmailBackupToken.del(admin.id) + end + end + + it "returns 422 when token is bad" do + begin + path = File.join(Backup.base_directory, backup_filename) + File.open(path, "w") { |f| f.write("hello") } + + Backup.create_from_filename(backup_filename) + + get "/admin/backups/#{backup_filename}.json", params: { token: "bad_value" } + + expect(response.status).to eq(422) + expect(response.headers['Content-Disposition']).not_to match(/attachment; filename/) + ensure + File.delete(path) + end + end + + it "returns 404 when the backup does not exist" do + token = EmailBackupToken.set(admin.id) + get "/admin/backups/#{backup_filename}.json", params: { token: token } + + EmailBackupToken.del(admin.id) + expect(response.status).to eq(404) + end + end + + describe '#destroy' do + let(:b) { Backup.new(backup_filename) } + + it "removes the backup if found" do + begin + path = File.join(Backup.base_directory, backup_filename) + File.open(path, "w") { |f| f.write("hello") } + + Backup.create_from_filename(backup_filename) + + expect do + delete "/admin/backups/#{backup_filename}.json" + end.to change { UserHistory.where(action: UserHistory.actions[:backup_destroy]).count }.by(1) + + expect(response.status).to eq(200) + expect(File.exists?(path)).to eq(false) + ensure + File.delete(path) if File.exists?(path) + end + end + + it "doesn't remove the backup if not found" do + delete "/admin/backups/#{backup_filename}.json" + expect(response.status).to eq(404) + end + end + + describe '#logs' do + it "preloads important data" do + get "/admin/backups/logs.html" + expect(response.status).to eq(200) + + preloaded = controller.instance_variable_get("@preloaded").map do |key, value| + [key, JSON.parse(value)] + end.to_h + + expect(preloaded["operations_status"].symbolize_keys).to eq(BackupRestore.operations_status) + expect(preloaded["logs"].size).to eq(BackupRestore.logs.size) + end + end + + describe '#restore' do + it "starts a restore" do + expect(SiteSetting.disable_emails).to eq("no") + BackupRestore.expects(:restore!).with(admin.id, filename: backup_filename, publish_to_message_bus: true, client_id: "foo") + + post "/admin/backups/#{backup_filename}/restore.json", params: { client_id: "foo" } + + expect(SiteSetting.disable_emails).to eq("yes") + expect(response.status).to eq(200) + end + end + + describe '#readonly' do + it "enables readonly mode" do + expect(Discourse.readonly_mode?).to eq(false) + + expect { put "/admin/backups/readonly.json", params: { enable: true } } + .to change { UserHistory.where(action: UserHistory.actions[:change_readonly_mode], new_value: "t").count }.by(1) + + expect(Discourse.readonly_mode?).to eq(true) + expect(response.status).to eq(200) + end + + it "disables readonly mode" do + Discourse.enable_readonly_mode(Discourse::USER_READONLY_MODE_KEY) + expect(Discourse.readonly_mode?).to eq(true) + + expect { put "/admin/backups/readonly.json", params: { enable: false } } + .to change { UserHistory.where(action: UserHistory.actions[:change_readonly_mode], new_value: "f").count }.by(1) + + expect(response.status).to eq(200) + expect(Discourse.readonly_mode?).to eq(false) + end + end + + describe "#upload_backup_chunk" do + describe "when filename contains invalid characters" do + it "should raise an error" do + ['灰色.tar.gz', '; echo \'haha\'.tar.gz'].each do |invalid_filename| + described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) + + post "/admin/backups/upload", params: { + resumableFilename: invalid_filename, resumableTotalSize: 1 + } + + expect(response.status).to eq(415) + expect(response.body).to eq(I18n.t('backup.invalid_filename')) + end + end + end + + describe "when filename is valid" do + it "should upload the file successfully" do + begin + described_class.any_instance.expects(:has_enough_space_on_disk?).returns(true) + + filename = 'test_Site-0123456789.tar.gz' + + post "/admin/backups/upload.json", params: { + resumableFilename: filename, + resumableTotalSize: 1, + resumableIdentifier: 'test', + resumableChunkNumber: '1', + resumableChunkSize: '1', + resumableCurrentChunkSize: '1', + file: fixture_file_upload(Tempfile.new) + } + + expect(response.status).to eq(200) + expect(response.body).to eq("") + ensure + begin + File.delete( + File.join(Backup.base_directory, 'tmp', 'test', "#{filename}.part1") + ) + rescue Errno::ENOENT + end + end + end end end @@ -21,7 +259,7 @@ RSpec.describe Admin::BackupsController do post "/admin/backups/rollback.json" - expect(response).to be_success + expect(response.status).to eq(200) end it 'should not allow rollback via a GET request' do @@ -36,7 +274,7 @@ RSpec.describe Admin::BackupsController do delete "/admin/backups/cancel.json" - expect(response).to be_success + expect(response.status).to eq(200) end it 'should not allow cancel via a GET request' do @@ -59,7 +297,7 @@ RSpec.describe Admin::BackupsController do put "/admin/backups/#{backup_filename}.json" - expect(response).to be_success + expect(response.status).to eq(200) end it "returns 404 when the backup does not exist" do @@ -67,6 +305,5 @@ RSpec.describe Admin::BackupsController do expect(response).to be_not_found end - end end diff --git a/spec/controllers/admin/badges_controller_spec.rb b/spec/requests/admin/badges_controller_spec.rb similarity index 67% rename from spec/controllers/admin/badges_controller_spec.rb rename to spec/requests/admin/badges_controller_spec.rb index d5995aac40..add572d95f 100644 --- a/spec/controllers/admin/badges_controller_spec.rb +++ b/spec/requests/admin/badges_controller_spec.rb @@ -1,60 +1,62 @@ require 'rails_helper' describe Admin::BadgesController do - context "while logged in as an admin" do - let!(:user) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } let!(:badge) { Fabricate(:badge) } - context 'index' do + before do + sign_in(admin) + end + + describe '#index' do it 'returns badge index' do - get :index, format: :json - expect(response).to be_success + get "/admin/badges.json" + expect(response.status).to eq(200) end end - context 'preview' do + describe '#preview' do it 'allows preview enable_badge_sql is enabled' do SiteSetting.enable_badge_sql = true - get :preview, params: { + post "/admin/badges/preview.json", params: { sql: 'select id as user_id, created_at granted_at from users' - }, format: :json + } + expect(response.status).to eq(200) expect(JSON.parse(response.body)["grant_count"]).to be > 0 end + it 'does not allow anything if enable_badge_sql is disabled' do SiteSetting.enable_badge_sql = false - get :preview, params: { + post "/admin/badges/preview.json", params: { sql: 'select id as user_id, created_at granted_at from users' - }, format: :json + } expect(response.status).to eq(403) end end - describe '.create' do - render_views - + describe '#create' do it 'can create badges correctly' do SiteSetting.enable_badge_sql = true - post :create, params: { + post "/admin/badges.json", params: { name: 'test', query: 'select 1 as user_id, null as granted_at', badge_type_id: 1 - }, format: :json + } json = JSON.parse(response.body) expect(response.status).to eq(200) expect(json["badge"]["name"]).to eq('test') expect(json["badge"]["query"]).to eq('select 1 as user_id, null as granted_at') - expect(UserHistory.where(acting_user_id: user.id, action: UserHistory.actions[:create_badge]).exists?).to eq(true) + expect(UserHistory.where(acting_user_id: admin.id, action: UserHistory.actions[:create_badge]).exists?).to eq(true) end end - context '.save_badge_groupings' do - + describe '#save_badge_groupings' do it 'can save badge groupings' do groupings = BadgeGrouping.all.order(:position).to_a groupings << BadgeGrouping.new(name: 'Test 1') @@ -65,7 +67,8 @@ describe Admin::BadgesController do names = groupings.map { |g| g.name } ids = groupings.map { |g| g.id.to_s } - post :save_badge_groupings, params: { ids: ids, names: names }, format: :json + post "/admin/badges/badge_groupings.json", params: { ids: ids, names: names } + expect(response.status).to eq(200) groupings2 = BadgeGrouping.all.order(:position).to_a @@ -75,40 +78,38 @@ describe Admin::BadgesController do end end - context '.badge_types' do + describe '#badge_types' do it 'returns JSON' do - get :badge_types, format: :json + get "/admin/badges/types.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)["badge_types"]).to be_present end end - context '.destroy' do + describe '#destroy' do it 'deletes the badge' do - delete :destroy, params: { id: badge.id }, format: :json - expect(response).to be_success + delete "/admin/badges/#{badge.id}.json" + expect(response.status).to eq(200) expect(Badge.where(id: badge.id).exists?).to eq(false) - expect(UserHistory.where(acting_user_id: user.id, action: UserHistory.actions[:delete_badge]).exists?).to eq(true) + expect(UserHistory.where(acting_user_id: admin.id, action: UserHistory.actions[:delete_badge]).exists?).to eq(true) end end - context '.update' do - + describe '#update' do it 'does not update the name of system badges' do editor_badge = Badge.find(Badge::Editor) editor_badge_name = editor_badge.name - put :update, params: { - id: editor_badge.id, + put "/admin/badges/#{editor_badge.id}.json", params: { name: "123456" - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) editor_badge.reload expect(editor_badge.name).to eq(editor_badge_name) - expect(UserHistory.where(acting_user_id: user.id, action: UserHistory.actions[:change_badge]).exists?).to eq(true) + expect(UserHistory.where(acting_user_id: admin.id, action: UserHistory.actions[:change_badge]).exists?).to eq(true) end it 'does not allow query updates if badge_sql is disabled' do @@ -117,17 +118,16 @@ describe Admin::BadgesController do SiteSetting.enable_badge_sql = false - put :update, params: { - id: badge.id, + put "/admin/badges/#{badge.id}.json", params: { name: "123456", query: "select id user_id, created_at granted_at from users", badge_type_id: badge.badge_type_id, allow_title: false, multiple_grant: false, enabled: true - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) badge.reload expect(badge.name).to eq('123456') expect(badge.query).to eq('select 123') @@ -137,17 +137,16 @@ describe Admin::BadgesController do SiteSetting.enable_badge_sql = true sql = "select id user_id, created_at granted_at from users" - put :update, params: { - id: badge.id, + put "/admin/badges/#{badge.id}.json", params: { name: "123456", query: sql, badge_type_id: badge.badge_type_id, allow_title: false, multiple_grant: false, enabled: true - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) badge.reload expect(badge.name).to eq('123456') expect(badge.query).to eq(sql) diff --git a/spec/controllers/admin/color_schemes_controller_spec.rb b/spec/requests/admin/color_schemes_controller_spec.rb similarity index 51% rename from spec/controllers/admin/color_schemes_controller_spec.rb rename to spec/requests/admin/color_schemes_controller_spec.rb index 6b11e219fb..f3c59e3c01 100644 --- a/spec/controllers/admin/color_schemes_controller_spec.rb +++ b/spec/requests/admin/color_schemes_controller_spec.rb @@ -6,7 +6,7 @@ describe Admin::ColorSchemesController do end context "while logged in as an admin" do - let!(:user) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } let(:valid_params) { { color_scheme: { name: 'Such Design', colors: [ @@ -16,21 +16,26 @@ describe Admin::ColorSchemesController do } } } - describe "index" do - it "returns JSON" do - Fabricate(:color_scheme) - get :index, format: :json + before do + sign_in(admin) + end - expect(response).to be_success - expect(::JSON.parse(response.body)).to be_present + describe "#index" do + it "returns JSON" do + scheme_name = Fabricate(:color_scheme).name + get "/admin/color_schemes.json" + + expect(response.status).to eq(200) + schemes = JSON.parse(response.body).map { |scheme| scheme["name"] } + expect(schemes).to include(scheme_name) end end - describe "create" do + describe "#create" do it "returns JSON" do - post :create, params: valid_params, format: :json + post "/admin/color_schemes.json", params: valid_params - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['id']).to be_present end @@ -38,47 +43,50 @@ describe Admin::ColorSchemesController do params = valid_params params[:color_scheme][:colors][0][:hex] = 'cool color please' - post :create, params: valid_params, format: :json + post "/admin/color_schemes.json", params: valid_params - expect(response).not_to be_success + expect(response.status).to eq(422) expect(::JSON.parse(response.body)['errors']).to be_present end end - describe "update" do + describe "#update" do let(:existing) { Fabricate(:color_scheme) } it "returns success" do - ColorSchemeRevisor.expects(:revise).returns(existing) - put :update, params: valid_params.merge(id: existing.id), format: :json - expect(response).to be_success - end + put "/admin/color_schemes/#{existing.id}.json", params: valid_params + expect(response.status).to eq(200) - it "returns JSON" do - ColorSchemeRevisor.expects(:revise).returns(existing) - put :update, params: valid_params.merge(id: existing.id), format: :json - expect(::JSON.parse(response.body)['id']).to be_present + existing.reload + new_colors = valid_params[:color_scheme][:colors] + updated_colors = existing.colors.map { |color| { name: color.name, hex: color.hex } } + + expect(new_colors & updated_colors).to eq(new_colors) + expect(existing.name).to eq(valid_params[:color_scheme][:name]) end it "returns failure with invalid params" do color_scheme = Fabricate(:color_scheme) - params = valid_params.merge(id: color_scheme.id) + params = valid_params + params[:color_scheme][:colors][0][:name] = color_scheme.colors.first.name params[:color_scheme][:colors][0][:hex] = 'cool color please' - put :update, params: params, format: :json - expect(response).not_to be_success + + put "/admin/color_schemes/#{color_scheme.id}.json", params: params + + expect(response.status).to eq(422) expect(::JSON.parse(response.body)['errors']).to be_present end end - describe "destroy" do + describe "#destroy" do let!(:existing) { Fabricate(:color_scheme) } it "returns success" do expect { - delete :destroy, params: { id: existing.id }, format: :json + delete "/admin/color_schemes/#{existing.id}.json" }.to change { ColorScheme.count }.by(-1) - expect(response).to be_success + expect(response.status).to eq(200) end end end diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/requests/admin/dashboard_controller_spec.rb similarity index 78% rename from spec/controllers/admin/dashboard_controller_spec.rb rename to spec/requests/admin/dashboard_controller_spec.rb index c190f72b7e..5d26979267 100644 --- a/spec/controllers/admin/dashboard_controller_spec.rb +++ b/spec/requests/admin/dashboard_controller_spec.rb @@ -12,18 +12,22 @@ describe Admin::DashboardController do end context 'while logged in as an admin' do - let!(:admin) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } - context '.index' do + before do + sign_in(admin) + end + + describe '#index' do context 'version checking is enabled' do before do SiteSetting.version_checks = true end it 'returns discourse version info' do - get :index, format: :json + get "/admin/dashboard.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['version_check']).to be_present end end @@ -34,23 +38,24 @@ describe Admin::DashboardController do end it 'does not return discourse version info' do - get :index, format: :json + get "/admin/dashboard.json" + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json['version_check']).not_to be_present end end end - context '.problems' do + describe '#problems' do context 'when there are no problems' do before do AdminDashboardData.stubs(:fetch_problems).returns([]) end it 'returns an empty array' do - get :problems, format: :json + get "/admin/dashboard/problems.json" - expect(response).to be_success + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json['problems'].size).to eq(0) end @@ -62,7 +67,8 @@ describe Admin::DashboardController do end it 'returns an array of strings' do - get :problems, format: :json + get "/admin/dashboard/problems.json" + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json['problems'].size).to eq(2) expect(json['problems'][0]).to be_a(String) diff --git a/spec/controllers/admin/email_controller_spec.rb b/spec/requests/admin/email_controller_spec.rb similarity index 51% rename from spec/controllers/admin/email_controller_spec.rb rename to spec/requests/admin/email_controller_spec.rb index 0b0c2ffca2..ff8b46e71b 100644 --- a/spec/controllers/admin/email_controller_spec.rb +++ b/spec/requests/admin/email_controller_spec.rb @@ -1,26 +1,28 @@ require 'rails_helper' describe Admin::EmailController do + let(:admin) { Fabricate(:admin) } + + before do + sign_in(admin) + end it "is a subclass of AdminController" do expect(Admin::EmailController < Admin::AdminController).to eq(true) end - let!(:user) { log_in(:admin) } - - context '.index' do + describe '#index' do before do - subject + Admin::EmailController.any_instance .expects(:action_mailer_settings) .returns( username: 'username', password: 'secret' ) - - get :index, format: :json end it 'does not include the password in the response' do + get "/admin/email.json" mail_settings = JSON.parse(response.body)['settings'] expect( @@ -29,84 +31,74 @@ describe Admin::EmailController do end end - context '.sent' do - before do - get :sent, format: :json + describe '#sent' do + it "succeeds" do + get "/admin/email/sent.json" + expect(response.status).to eq(200) end - - subject { response } - it { is_expected.to be_success } end - context '.skipped' do - before do - get :skipped, format: :json + describe '#skipped' do + it "succeeds" do + get "/admin/email/skipped.json" + expect(response.status).to eq(200) end - - subject { response } - it { is_expected.to be_success } end - context '.test' do + describe '#test' do it 'raises an error without the email parameter' do - expect do - post :test, format: :json - end.to raise_error(ActionController::ParameterMissing) + post "/admin/email/test.json" + expect(response.status).to eq(400) end context 'with an email address' do it 'enqueues a test email job' do - job_mock = mock - Jobs::TestEmail.expects(:new).returns(job_mock) - job_mock.expects(:execute).with(to_address: 'eviltrout@test.domain') - post :test, params: { email_address: 'eviltrout@test.domain' }, format: :json + post "/admin/email/test.json", params: { email_address: 'eviltrout@test.domain' } + expect(response.status).to eq(200) + expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include('eviltrout@test.domain') end end end - context '.preview_digest' do + describe '#preview_digest' do it 'raises an error without the last_seen_at parameter' do - expect do - get :preview_digest, format: :json - end.to raise_error(ActionController::ParameterMissing) + get "/admin/email/preview-digest.json" + expect(response.status).to eq(400) end it "previews the digest" do - get :preview_digest, params: { - last_seen_at: 1.week.ago, username: user.username - }, format: :json - - expect(response).to be_success + get "/admin/email/preview-digest.json", params: { + last_seen_at: 1.week.ago, username: admin.username + } + expect(response.status).to eq(200) end end - context '#handle_mail' do - before do - log_in_user(Fabricate(:admin)) - end - + describe '#handle_mail' do it 'should enqueue the right job' do - expect { post :handle_mail, params: { email: email('cc') }, format: :json } + expect { post "/admin/email/handle_mail.json", params: { email: email('cc') } } .to change { Jobs::ProcessEmail.jobs.count }.by(1) + expect(response.status).to eq(200) end end - context '.rejected' do + describe '#rejected' do it 'should provide a string for a blank error' do Fabricate(:incoming_email, error: "") - get :rejected, format: :json + get "/admin/email/rejected.json" + expect(response.status).to eq(200) rejected = JSON.parse(response.body) expect(rejected.first['error']).to eq(I18n.t("emails.incoming.unrecognized_error")) end end - context '.incoming' do + describe '#incoming' do it 'should provide a string for a blank error' do incoming_email = Fabricate(:incoming_email, error: "") - get :incoming, params: { id: incoming_email.id }, format: :json + get "/admin/email/incoming/#{incoming_email.id}.json" + expect(response.status).to eq(200) incoming = JSON.parse(response.body) expect(incoming['error']).to eq(I18n.t("emails.incoming.unrecognized_error")) end end - end diff --git a/spec/requests/admin/email_templates_controller_spec.rb b/spec/requests/admin/email_templates_controller_spec.rb index 9babb31b38..b37a5e65a2 100644 --- a/spec/requests/admin/email_templates_controller_spec.rb +++ b/spec/requests/admin/email_templates_controller_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Admin::EmailTemplatesController do sign_in(admin) get '/admin/customize/email_templates.json' - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json['email_templates']).to be_present @@ -66,7 +66,7 @@ RSpec.describe Admin::EmailTemplatesController do email_template: { subject: 'Foo', body: 'Bar' } }, headers: headers - expect(response).not_to be_success + expect(response).not_to be_successful json = ::JSON.parse(response.body) expect(json['error_type']).to eq('not_found') @@ -169,7 +169,7 @@ RSpec.describe Admin::EmailTemplatesController do email_template: { subject: email_subject, body: email_body } }, headers: headers - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present @@ -237,7 +237,7 @@ RSpec.describe Admin::EmailTemplatesController do it "returns 'not found' when an unknown email template id is used" do delete '/admin/customize/email_templates/non_existent_template', headers: headers - expect(response).not_to be_success + expect(response).not_to be_successful json = ::JSON.parse(response.body) expect(json['error_type']).to eq('not_found') @@ -265,7 +265,7 @@ RSpec.describe Admin::EmailTemplatesController do it "returns the restored email template" do delete '/admin/customize/email_templates/user_notifications.admin_login', headers: headers - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present diff --git a/spec/controllers/admin/embeddable_hosts_controller_spec.rb b/spec/requests/admin/embeddable_hosts_controller_spec.rb similarity index 99% rename from spec/controllers/admin/embeddable_hosts_controller_spec.rb rename to spec/requests/admin/embeddable_hosts_controller_spec.rb index 54a97ed938..403ed76e1e 100644 --- a/spec/controllers/admin/embeddable_hosts_controller_spec.rb +++ b/spec/requests/admin/embeddable_hosts_controller_spec.rb @@ -1,9 +1,7 @@ require 'rails_helper' describe Admin::EmbeddableHostsController do - it "is a subclass of AdminController" do expect(Admin::EmbeddableHostsController < Admin::AdminController).to eq(true) end - end diff --git a/spec/controllers/admin/embedding_controller_spec.rb b/spec/requests/admin/embedding_controller_spec.rb similarity index 98% rename from spec/controllers/admin/embedding_controller_spec.rb rename to spec/requests/admin/embedding_controller_spec.rb index 49a989e15b..9531c3a618 100644 --- a/spec/controllers/admin/embedding_controller_spec.rb +++ b/spec/requests/admin/embedding_controller_spec.rb @@ -1,9 +1,7 @@ require 'rails_helper' describe Admin::EmbeddingController do - it "is a subclass of AdminController" do expect(Admin::EmbeddingController < Admin::AdminController).to eq(true) end - end diff --git a/spec/requests/admin/emojis_controller_spec.rb b/spec/requests/admin/emojis_controller_spec.rb index 3d781af215..24019fc343 100644 --- a/spec/requests/admin/emojis_controller_spec.rb +++ b/spec/requests/admin/emojis_controller_spec.rb @@ -8,6 +8,20 @@ RSpec.describe Admin::EmojisController do sign_in(admin) end + describe '#index' do + it "returns a list of custom emojis" do + CustomEmoji.create!(name: 'osama-test-emoji', upload: upload) + Emoji.clear_cache + + get "/admin/customize/emojis.json" + expect(response.status).to eq(200) + + json = ::JSON.parse(response.body) + expect(json[0]["name"]).to eq("osama-test-emoji") + expect(json[0]["url"]).to eq(upload.url) + end + end + describe "#create" do describe 'when upload is invalid' do it 'should publish the right error' do diff --git a/spec/requests/admin/flagged_topics_controller_spec.rb b/spec/requests/admin/flagged_topics_controller_spec.rb index 8f768294bc..3d27ac33af 100644 --- a/spec/requests/admin/flagged_topics_controller_spec.rb +++ b/spec/requests/admin/flagged_topics_controller_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Admin::FlaggedTopicsController do shared_examples "successfully retrieve list of flagged topics" do it "returns a list of flagged topics" do get "/admin/flagged_topics.json" - expect(response).to be_success + expect(response.status).to eq(200) data = ::JSON.parse(response.body) expect(data['flagged_topics']).to be_present diff --git a/spec/requests/admin/flags_controller_spec.rb b/spec/requests/admin/flags_controller_spec.rb index 98ee91c851..c8ebc0ea77 100644 --- a/spec/requests/admin/flags_controller_spec.rb +++ b/spec/requests/admin/flags_controller_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Admin::FlagsController do it 'should return the right response when nothing is flagged' do get '/admin/flags.json' - expect(response).to be_success + expect(response.status).to eq(200) data = ::JSON.parse(response.body) expect(data["users"]).to eq([]) @@ -25,11 +25,11 @@ RSpec.describe Admin::FlagsController do get '/admin/flags.json' - expect(response).to be_success + expect(response.status).to eq(200) data = ::JSON.parse(response.body) - data["users"].length == 2 - data["posts"].length == 1 + expect(data["users"].length).to eq(2) + expect(data["posts"].length).to eq(1) end end @@ -49,9 +49,8 @@ RSpec.describe Admin::FlagsController do expect(post_action.agreed_by_id).to eq(admin.id) - post_1 = Post.offset(1).last - - expect(post_1.raw).to eq(I18n.with_locale(:en) { I18n.t('flags_dispositions.agreed') }) + agree_post = Topic.joins(:topic_allowed_users).where('topic_allowed_users.user_id = ?', user.id).order(:id).last.posts.last + expect(agree_post.raw).to eq(I18n.with_locale(:en) { I18n.t('flags_dispositions.agreed') }) end end end diff --git a/spec/requests/admin/impersonate_controller_spec.rb b/spec/requests/admin/impersonate_controller_spec.rb new file mode 100644 index 0000000000..3cb2657330 --- /dev/null +++ b/spec/requests/admin/impersonate_controller_spec.rb @@ -0,0 +1,62 @@ +require 'rails_helper' + +describe Admin::ImpersonateController do + + it "is a subclass of AdminController" do + expect(Admin::ImpersonateController < Admin::AdminController).to eq(true) + end + + context 'while logged in as an admin' do + let(:admin) { Fabricate(:admin) } + let(:user) { Fabricate(:user) } + let(:another_admin) { Fabricate(:admin) } + + before do + sign_in(admin) + end + + describe '#index' do + it 'returns success' do + get "/admin/impersonate.json" + expect(response.status).to eq(200) + end + end + + describe '#create' do + it 'requires a username_or_email parameter' do + post "/admin/impersonate.json" + expect(response.status).to eq(400) + expect(session[:current_user_id]).to eq(admin.id) + end + + it 'returns 404 when that user does not exist' do + post "/admin/impersonate.json", params: { username_or_email: 'hedonismbot' } + expect(response.status).to eq(404) + expect(session[:current_user_id]).to eq(admin.id) + end + + it "raises an invalid access error if the user can't be impersonated" do + post "/admin/impersonate.json", params: { username_or_email: another_admin.email } + expect(response.status).to eq(403) + expect(session[:current_user_id]).to eq(admin.id) + end + + context 'success' do + it "succeeds and logs the impersonation" do + expect do + post "/admin/impersonate.json", params: { username_or_email: user.username } + end.to change { UserHistory.where(action: UserHistory.actions[:impersonate]).count }.by(1) + + expect(response.status).to eq(200) + expect(session[:current_user_id]).to eq(user.id) + end + + it "also works with an email address" do + post "/admin/impersonate.json", params: { username_or_email: user.email } + expect(response.status).to eq(200) + expect(session[:current_user_id]).to eq(user.id) + end + end + end + end +end diff --git a/spec/requests/admin/moderation_history_controller_spec.rb b/spec/requests/admin/moderation_history_controller_spec.rb index f16d340fae..b35989ad30 100644 --- a/spec/requests/admin/moderation_history_controller_spec.rb +++ b/spec/requests/admin/moderation_history_controller_spec.rb @@ -10,28 +10,28 @@ RSpec.describe Admin::BackupsController do describe "parameters" do it "returns 404 without a valid filter" do get "/admin/moderation_history.json" - expect(response).not_to be_success + expect(response).not_to be_successful end it "returns 404 without a valid id" do get "/admin/moderation_history.json?filter=topic" - expect(response).not_to be_success + expect(response).not_to be_successful end end describe "for a post" do it "returns an empty array when the post doesn't exist" do get "/admin/moderation_history.json?filter=post&post_id=99999999" - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['moderation_history']).to be_blank end it "returns a history when the post exists" do p = Fabricate(:post) - p = Fabricate(:post, topic_id: p.topic_id) + p = Fabricate(:post, topic: p.topic) PostDestroyer.new(Discourse.system_user, p).destroy get "/admin/moderation_history.json?filter=post&post_id=#{p.id}" - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['moderation_history']).to be_present end @@ -40,7 +40,7 @@ RSpec.describe Admin::BackupsController do describe "for a topic" do it "returns empty history when the topic doesn't exist" do get "/admin/moderation_history.json?filter=topic&topic_id=1234" - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['moderation_history']).to be_blank end @@ -48,7 +48,7 @@ RSpec.describe Admin::BackupsController do p = Fabricate(:post) PostDestroyer.new(Discourse.system_user, p).destroy get "/admin/moderation_history.json?filter=topic&topic_id=#{p.topic_id}" - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['moderation_history']).to be_present end end diff --git a/spec/controllers/admin/permalinks_controller_spec.rb b/spec/requests/admin/permalinks_controller_spec.rb similarity index 76% rename from spec/controllers/admin/permalinks_controller_spec.rb rename to spec/requests/admin/permalinks_controller_spec.rb index 1075150ab1..c0fa35abde 100644 --- a/spec/controllers/admin/permalinks_controller_spec.rb +++ b/spec/requests/admin/permalinks_controller_spec.rb @@ -6,18 +6,22 @@ describe Admin::PermalinksController do expect(Admin::PermalinksController < Admin::AdminController).to eq(true) end - let!(:user) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } - describe 'index' do + before do + sign_in(admin) + end + + describe '#index' do it 'filters url' do Fabricate(:permalink, url: "/forum/23") Fabricate(:permalink, url: "/forum/98") Fabricate(:permalink, url: "/discuss/topic/45") Fabricate(:permalink, url: "/discuss/topic/76") - get :index, params: { filter: "topic" }, format: :json + get "/admin/permalinks.json", params: { filter: "topic" } - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body) expect(result.length).to eq(2) end @@ -28,9 +32,9 @@ describe Admin::PermalinksController do Fabricate(:permalink, external_url: "http://www.discourse.org") Fabricate(:permalink, external_url: "http://try.discourse.org") - get :index, params: { filter: "discourse" }, format: :json + get "/admin/permalinks.json", params: { filter: "discourse" } - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body) expect(result.length).to eq(2) end @@ -41,9 +45,9 @@ describe Admin::PermalinksController do Fabricate(:permalink, url: "/discuss/topic/45", external_url: "http://discourse.org") Fabricate(:permalink, url: "/discuss/topic/76", external_url: "http://try.discourse.org") - get :index, params: { filter: "discourse" }, format: :json + get "/admin/permalinks.json", params: { filter: "discourse" } - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body) expect(result.length).to eq(3) end diff --git a/spec/controllers/admin/plugins_controller_spec.rb b/spec/requests/admin/plugins_controller_spec.rb similarity index 73% rename from spec/controllers/admin/plugins_controller_spec.rb rename to spec/requests/admin/plugins_controller_spec.rb index 9df2c2737e..57175ebbb4 100644 --- a/spec/controllers/admin/plugins_controller_spec.rb +++ b/spec/requests/admin/plugins_controller_spec.rb @@ -7,13 +7,14 @@ describe Admin::PluginsController do end context "while logged in as an admin" do - let!(:admin) { log_in(:admin) } + before do + sign_in(Fabricate(:admin)) + end it 'should return JSON' do - get :index, format: :json - expect(response).to be_success + get "/admin/plugins.json" + expect(response.status).to eq(200) expect(::JSON.parse(response.body).has_key?('plugins')).to eq(true) end end - end diff --git a/spec/controllers/admin/reports_controller_spec.rb b/spec/requests/admin/reports_controller_spec.rb similarity index 51% rename from spec/controllers/admin/reports_controller_spec.rb rename to spec/requests/admin/reports_controller_spec.rb index 740df64afb..27795f6579 100644 --- a/spec/controllers/admin/reports_controller_spec.rb +++ b/spec/requests/admin/reports_controller_spec.rb @@ -6,68 +6,51 @@ describe Admin::ReportsController do end context 'while logged in as an admin' do - let!(:admin) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } let(:user) { Fabricate(:user) } - context '.show' do + before do + sign_in(admin) + end + describe '#show' do context "invalid id form" do let(:invalid_id) { "!!&asdfasdf" } - it "never calls Report.find" do - Report.expects(:find).never - get :show, params: { type: invalid_id }, format: :json - end - it "returns 404" do - get :show, params: { type: invalid_id }, format: :json + get "/admin/reports/#{invalid_id}.json" expect(response.status).to eq(404) end end context "valid type form" do - context 'missing report' do - before do - Report.expects(:find).with('active', instance_of(Hash)).returns(nil) - get :show, params: { type: 'active' }, format: :json - end - - it "renders the report as JSON" do + it "returns a 404 error" do + get "/admin/reports/nonexistent.json" expect(response.status).to eq(404) end end context 'a report is found' do - before do - Report.expects(:find).with('active', instance_of(Hash)).returns(Report.new('active')) - get :show, params: { type: 'active' }, format: :json - end - it "renders the report as JSON" do - expect(response).to be_success - end + Fabricate(:topic) + get "/admin/reports/topics.json" - it "renders the report as JSON" do - expect(::JSON.parse(response.body)).to be_present + expect(response.status).to eq(200) + expect(JSON.parse(response.body)["report"]["total"]).to eq(1) end - end - end describe 'when report is scoped to a category' do let(:category) { Fabricate(:category) } - let(:topic) { Fabricate(:topic, category: category) } - let(:other_topic) { Fabricate(:topic) } + let!(:topic) { Fabricate(:topic, category: category) } + let!(:other_topic) { Fabricate(:topic) } it 'should render the report as JSON' do - topic - other_topic + get "/admin/reports/topics.json", params: { category_id: category.id } - get :show, params: { type: 'topics', category_id: category.id }, format: :json - - expect(response).to be_success + expect(response.status).to eq(200) report = JSON.parse(response.body)["report"] @@ -78,16 +61,15 @@ describe Admin::ReportsController do describe 'when report is scoped to a group' do let(:user) { Fabricate(:user) } - let(:other_user) { Fabricate(:user) } + let!(:other_user) { Fabricate(:user) } let(:group) { Fabricate(:group) } it 'should render the report as JSON' do - other_user group.add(user) - get :show, params: { type: 'signups', group_id: group.id }, format: :json + get "/admin/reports/signups.json", params: { group_id: group.id } - expect(response).to be_success + expect(response.status).to eq(200) report = JSON.parse(response.body)["report"] @@ -95,9 +77,6 @@ describe Admin::ReportsController do expect(report["data"].count).to eq(1) end end - end - end - end diff --git a/spec/controllers/admin/screened_emails_controller_spec.rb b/spec/requests/admin/screened_emails_controller_spec.rb similarity index 51% rename from spec/controllers/admin/screened_emails_controller_spec.rb rename to spec/requests/admin/screened_emails_controller_spec.rb index fb4486a9b4..8d9ce25032 100644 --- a/spec/controllers/admin/screened_emails_controller_spec.rb +++ b/spec/requests/admin/screened_emails_controller_spec.rb @@ -5,18 +5,17 @@ describe Admin::ScreenedEmailsController do expect(Admin::ScreenedEmailsController < Admin::AdminController).to eq(true) end - let!(:user) { log_in(:admin) } - - context '.index' do + describe '#index' do before do - get :index, format: :json + sign_in(Fabricate(:admin)) end - subject { response } - it { is_expected.to be_success } - it 'returns JSON' do - expect(::JSON.parse(subject.body)).to be_a(Array) + Fabricate(:screened_email) + get "/admin/logs/screened_emails.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json.size).to eq(1) end end end diff --git a/spec/controllers/admin/screened_ip_addresses_controller_spec.rb b/spec/requests/admin/screened_ip_addresses_controller_spec.rb similarity index 72% rename from spec/controllers/admin/screened_ip_addresses_controller_spec.rb rename to spec/requests/admin/screened_ip_addresses_controller_spec.rb index 664fc85cdb..7737b1ee53 100644 --- a/spec/controllers/admin/screened_ip_addresses_controller_spec.rb +++ b/spec/requests/admin/screened_ip_addresses_controller_spec.rb @@ -6,33 +6,34 @@ describe Admin::ScreenedIpAddressesController do expect(Admin::ScreenedIpAddressesController < Admin::AdminController).to eq(true) end - let!(:user) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } - describe 'index' do + before do + sign_in(admin) + end + describe '#index' do it 'filters screened ip addresses' do Fabricate(:screened_ip_address, ip_address: "1.2.3.4") Fabricate(:screened_ip_address, ip_address: "1.2.3.5") Fabricate(:screened_ip_address, ip_address: "1.2.3.6") Fabricate(:screened_ip_address, ip_address: "4.5.6.7") - get :index, params: { filter: "1.2.*" }, format: :json + get "/admin/logs/screened_ip_addresses.json", params: { filter: "1.2.*" } - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body) expect(result.length).to eq(3) - get :index, params: { filter: "4.5.6.7" }, format: :json + get "/admin/logs/screened_ip_addresses.json", params: { filter: "4.5.6.7" } - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body) expect(result.length).to eq(1) end - end - describe 'roll_up' do - + describe '#roll_up' do it "rolls up 1.2.3.* entries" do Fabricate(:screened_ip_address, ip_address: "1.2.3.4", match_count: 1) Fabricate(:screened_ip_address, ip_address: "1.2.3.5", match_count: 1) @@ -41,11 +42,13 @@ describe Admin::ScreenedIpAddressesController do Fabricate(:screened_ip_address, ip_address: "42.42.42.4", match_count: 1) Fabricate(:screened_ip_address, ip_address: "42.42.42.5", match_count: 1) - StaffActionLogger.any_instance.expects(:log_roll_up) SiteSetting.min_ban_entries_for_roll_up = 3 - post :roll_up, format: :json - expect(response).to be_success + expect do + post "/admin/logs/screened_ip_addresses/roll_up.json" + end.to change { UserHistory.where(action: UserHistory.actions[:roll_up]).count }.by(1) + + expect(response.status).to eq(200) subnet = ScreenedIpAddress.where(ip_address: "1.2.3.0/24").first expect(subnet).to be_present @@ -61,17 +64,17 @@ describe Admin::ScreenedIpAddressesController do Fabricate(:screened_ip_address, ip_address: "1.2.42.0/24", match_count: 1) - StaffActionLogger.any_instance.expects(:log_roll_up) SiteSetting.min_ban_entries_for_roll_up = 5 - post :roll_up, format: :json - expect(response).to be_success + expect do + post "/admin/logs/screened_ip_addresses/roll_up.json" + end.to change { UserHistory.where(action: UserHistory.actions[:roll_up]).count }.by(1) + + expect(response.status).to eq(200) subnet = ScreenedIpAddress.where(ip_address: "1.2.0.0/16").first expect(subnet).to be_present expect(subnet.match_count).to eq(6) end - end - end diff --git a/spec/controllers/admin/screened_urls_controller_spec.rb b/spec/requests/admin/screened_urls_controller_spec.rb similarity index 51% rename from spec/controllers/admin/screened_urls_controller_spec.rb rename to spec/requests/admin/screened_urls_controller_spec.rb index 86d7fce0b7..e4d4ba05e9 100644 --- a/spec/controllers/admin/screened_urls_controller_spec.rb +++ b/spec/requests/admin/screened_urls_controller_spec.rb @@ -5,18 +5,17 @@ describe Admin::ScreenedUrlsController do expect(Admin::ScreenedUrlsController < Admin::AdminController).to eq(true) end - let!(:user) { log_in(:admin) } - - context '.index' do + describe '#index' do before do - get :index, format: :json + sign_in(Fabricate(:admin)) end - subject { response } - it { is_expected.to be_success } - it 'returns JSON' do - expect(::JSON.parse(subject.body)).to be_a(Array) + Fabricate(:screened_url) + get "/admin/logs/screened_urls.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json.size).to eq(1) end end end diff --git a/spec/requests/admin/search_logs_spec.rb b/spec/requests/admin/search_logs_spec.rb index f811bfd44f..b234ea2abf 100644 --- a/spec/requests/admin/search_logs_spec.rb +++ b/spec/requests/admin/search_logs_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Admin::SearchLogsController do sign_in(admin) get '/admin/logs/search_logs.json' - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json[0]['term']).to eq('ruby') @@ -51,7 +51,7 @@ RSpec.describe Admin::SearchLogsController do sign_in(admin) get '/admin/logs/search_logs/term/ruby.json' - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json['term']['type']).to eq('search_log_term') diff --git a/spec/requests/admin/site_settings_controller_spec.rb b/spec/requests/admin/site_settings_controller_spec.rb new file mode 100644 index 0000000000..fd17c3dc39 --- /dev/null +++ b/spec/requests/admin/site_settings_controller_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +describe Admin::SiteSettingsController do + + it "is a subclass of AdminController" do + expect(Admin::SiteSettingsController < Admin::AdminController).to eq(true) + end + + context 'while logged in as an admin' do + let(:admin) { Fabricate(:admin) } + + before do + sign_in(admin) + end + + describe '#index' do + it 'returns valid info' do + get "/admin/site_settings.json" + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["site_settings"].length).to be > 100 + + locale = json["site_settings"].select do |s| + s["setting"] == "default_locale" + end + + expect(locale.length).to eq(1) + end + end + + describe '#update' do + before do + SiteSetting.setting(:test_setting, "default") + SiteSetting.refresh! + end + + it 'sets the value when the param is present' do + put "/admin/site_settings/test_setting.json", params: { + test_setting: 'hello' + } + expect(response.status).to eq(200) + expect(SiteSetting.test_setting).to eq('hello') + end + + it 'allows value to be a blank string' do + put "/admin/site_settings/test_setting.json", params: { + test_setting: '' + } + expect(response.status).to eq(200) + expect(SiteSetting.test_setting).to eq('') + end + + it 'logs the change' do + SiteSetting.test_setting = 'previous' + + expect do + put "/admin/site_settings/test_setting.json", params: { + test_setting: 'hello' + } + end.to change { UserHistory.where(action: UserHistory.actions[:change_site_setting]).count }.by(1) + + expect(response.status).to eq(200) + expect(SiteSetting.test_setting).to eq('hello') + end + + it 'does not allow changing of hidden settings' do + SiteSetting.setting(:hidden_setting, "hidden", hidden: true) + SiteSetting.refresh! + + put "/admin/site_settings/hidden_setting.json", params: { + hidden_setting: 'not allowed' + } + + expect(SiteSetting.hidden_setting).to eq("hidden") + expect(response.status).to eq(422) + end + + it 'fails when a setting does not exist' do + put "/admin/site_settings/provider.json", params: { provider: 'gotcha' } + expect(response.status).to eq(422) + end + end + end +end diff --git a/spec/requests/admin/site_texts_controller_spec.rb b/spec/requests/admin/site_texts_controller_spec.rb index f19c6015c8..0ee4383e23 100644 --- a/spec/requests/admin/site_texts_controller_spec.rb +++ b/spec/requests/admin/site_texts_controller_spec.rb @@ -9,7 +9,11 @@ RSpec.describe Admin::SiteTextsController do I18n.reload! end - context "#update" do + it "is a subclass of AdminController" do + expect(Admin::SiteTextsController < Admin::AdminController).to eq(true) + end + + context "when not logged in as an admin" do it "raises an error if you aren't logged in" do put '/admin/customize/site_texts/some_key.json', params: { site_text: { value: 'foo' } @@ -21,18 +25,47 @@ RSpec.describe Admin::SiteTextsController do it "raises an error if you aren't an admin" do sign_in(user) - put '/admin/customize/site_texts/some_key', params: { + put "/admin/customize/site_texts/some_key.json", params: { site_text: { value: 'foo' } } expect(response.status).to eq(404) end + end - context "when logged in as admin" do - before do - sign_in(admin) + context "when logged in as amin" do + before do + sign_in(admin) + end + + describe '#index' do + it 'returns json' do + get "/admin/customize/site_texts.json", params: { q: 'title' } + expect(response.status).to eq(200) + expect(::JSON.parse(response.body)).to be_present + end + end + + describe '#show' do + it 'returns a site text for a key that exists' do + get "/admin/customize/site_texts/js.topic.list.json" + expect(response.status).to eq(200) + + json = ::JSON.parse(response.body) + + site_text = json['site_text'] + + expect(site_text['id']).to eq('js.topic.list') + expect(site_text['value']).to eq(I18n.t("js.topic.list")) end + it 'returns not found for missing keys' do + get "/admin/customize/site_texts/made_up_no_key_exists.json" + expect(response.status).to eq(404) + end + end + + describe '#update & #revert' do it "returns 'not found' when an unknown key is used" do put '/admin/customize/site_texts/some_key.json', params: { site_text: { value: 'foo' } @@ -68,6 +101,106 @@ RSpec.describe Admin::SiteTextsController do json = ::JSON.parse(response.body) expect(json['error_type']).to eq('not_found') end + + it "returns the right error message" do + I18n.backend.store_translations(:en, some_key: '%{first} %{second}') + + put "/admin/customize/site_texts/some_key.json", params: { + site_text: { value: 'hello %{key} %{omg}' } + } + + expect(response.status).to eq(422) + + body = JSON.parse(response.body) + + expect(body['message']).to eq(I18n.t( + 'activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys', + keys: 'key, omg' + )) + end + + it 'logs the change' do + original_title = I18n.t(:title) + + put "/admin/customize/site_texts/title.json", params: { + site_text: { value: 'yay' } + } + expect(response.status).to eq(200) + + log = UserHistory.last + + expect(log.previous_value).to eq(original_title) + expect(log.new_value).to eq('yay') + expect(log.action).to eq(UserHistory.actions[:change_site_text]) + + delete "/admin/customize/site_texts/title.json" + expect(response.status).to eq(200) + + log = UserHistory.last + + expect(log.previous_value).to eq('yay') + expect(log.new_value).to eq(original_title) + expect(log.action).to eq(UserHistory.actions[:change_site_text]) + end + + it 'updates and reverts the key' do + orig_title = I18n.t(:title) + + put "/admin/customize/site_texts/title.json", params: { site_text: { value: 'hello' } } + expect(response.status).to eq(200) + + json = ::JSON.parse(response.body) + + site_text = json['site_text'] + + expect(site_text['id']).to eq('title') + expect(site_text['value']).to eq('hello') + + # Revert + delete "/admin/customize/site_texts/title.json" + expect(response.status).to eq(200) + + json = ::JSON.parse(response.body) + + site_text = json['site_text'] + + expect(site_text['id']).to eq('title') + expect(site_text['value']).to eq(orig_title) + end + + it 'returns site texts for the correct locale' do + SiteSetting.default_locale = :ru + + ru_title = 'title ru' + ru_mf_text = 'ru {NUM_RESULTS, plural, one {1 result} other {many} }' + + put "/admin/customize/site_texts/title.json", params: { site_text: { value: ru_title } } + expect(response.status).to eq(200) + put "/admin/customize/site_texts/js.topic.read_more_MF.json", params: { site_text: { value: ru_mf_text } } + expect(response.status).to eq(200) + + get "/admin/customize/site_texts/title.json" + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['site_text']['value']).to eq(ru_title) + + get "/admin/customize/site_texts/js.topic.read_more_MF.json" + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['site_text']['value']).to eq(ru_mf_text) + + SiteSetting.default_locale = :en + + get "/admin/customize/site_texts/title.json" + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['site_text']['value']).to_not eq(ru_title) + + get "/admin/customize/site_texts/js.topic.read_more_MF.json" + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['site_text']['value']).to_not eq(ru_mf_text) + end end end end diff --git a/spec/controllers/admin/staff_action_logs_controller_spec.rb b/spec/requests/admin/staff_action_logs_controller_spec.rb similarity index 73% rename from spec/controllers/admin/staff_action_logs_controller_spec.rb rename to spec/requests/admin/staff_action_logs_controller_spec.rb index 3da6dfa158..12acd22244 100644 --- a/spec/controllers/admin/staff_action_logs_controller_spec.rb +++ b/spec/requests/admin/staff_action_logs_controller_spec.rb @@ -5,29 +5,30 @@ describe Admin::StaffActionLogsController do expect(Admin::StaffActionLogsController < Admin::AdminController).to eq(true) end - let!(:user) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } - context '.index' do + before do + sign_in(admin) + end + describe '#index' do it 'generates logs' do - topic = Fabricate(:topic) - _record = StaffActionLogger.new(Discourse.system_user).log_topic_delete_recover(topic, "delete_topic") + StaffActionLogger.new(Discourse.system_user).log_topic_delete_recover(topic, "delete_topic") - get :index, params: { action_id: UserHistory.actions[:delete_topic] }, format: :json + get "/admin/logs/staff_action_logs.json", params: { action_id: UserHistory.actions[:delete_topic] } json = JSON.parse(response.body) - expect(response).to be_success + expect(response.status).to eq(200) expect(json["staff_action_logs"].length).to eq(1) expect(json["staff_action_logs"][0]["action_name"]).to eq("delete_topic") expect(json["user_history_actions"]).to include("id" => UserHistory.actions[:delete_topic], "name" => 'delete_topic') - end end - context '.diff' do + describe '#diff' do it 'can generate diffs for theme changes' do theme = Theme.new(user_id: -1, name: 'bob') theme.set_field(target: :mobile, name: :scss, value: 'body {.up}') @@ -40,8 +41,8 @@ describe Admin::StaffActionLogsController do record = StaffActionLogger.new(Discourse.system_user) .log_theme_change(original_json, theme) - get :diff, params: { id: record.id }, format: :json - expect(response).to be_success + get "/admin/logs/staff_action_logs/#{record.id}/diff.json" + expect(response.status).to eq(200) parsed = JSON.parse(response.body) expect(parsed["side_by_side"]).to include("up") diff --git a/spec/requests/admin/themes_controller_spec.rb b/spec/requests/admin/themes_controller_spec.rb new file mode 100644 index 0000000000..e0416191af --- /dev/null +++ b/spec/requests/admin/themes_controller_spec.rb @@ -0,0 +1,202 @@ +require 'rails_helper' +require_dependency 'theme_serializer' + +describe Admin::ThemesController do + let(:admin) { Fabricate(:admin) } + + it "is a subclass of AdminController" do + expect(Admin::UsersController < Admin::AdminController).to eq(true) + end + + before do + sign_in(admin) + end + + describe '#generate_key_pair' do + it 'can generate key pairs' do + post "/admin/themes/generate_key_pair.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["private_key"]).to include("RSA PRIVATE KEY") + expect(json["public_key"]).to include("ssh-rsa ") + end + end + + describe '#upload_asset' do + let(:upload) do + Rack::Test::UploadedFile.new(file_from_fixtures("fake.woff2", "woff2")) + end + + it 'can create a theme upload' do + post "/admin/themes/upload_asset.json", params: { file: upload } + expect(response.status).to eq(201) + + upload = Upload.find_by(original_filename: "fake.woff2") + + expect(upload.id).not_to be_nil + expect(JSON.parse(response.body)["upload_id"]).to eq(upload.id) + end + end + + describe '#import' do + let(:theme_file) do + Rack::Test::UploadedFile.new(file_from_fixtures("sam-s-simple-theme.dcstyle.json", "json")) + end + + let(:image) do + file_from_fixtures("logo.png") + end + + it 'can import a theme with an upload' do + upload = Fabricate(:upload) + theme = Theme.new(name: 'with-upload', user_id: -1) + upload = UploadCreator.new(image, "logo.png").create_for(-1) + theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var) + theme.save! + + json = ThemeWithEmbeddedUploadsSerializer.new(theme, root: 'theme').to_json + theme.destroy + + temp = Tempfile.new + temp.write(json) + temp.rewind + + uploaded_json = Rack::Test::UploadedFile.new(temp) + upload.destroy + + post "/admin/themes/import.json", params: { theme: uploaded_json } + expect(response.status).to eq(201) + temp.unlink + + theme = Theme.last + expect(theme.theme_fields.count).to eq(1) + expect(theme.theme_fields.first.upload).not_to eq(nil) + expect(theme.theme_fields.first.upload.filesize).to eq(upload.filesize) + expect(theme.theme_fields.first.upload.sha1).to eq(upload.sha1) + expect(theme.theme_fields.first.upload.original_filename).to eq(upload.original_filename) + end + + it 'imports a theme' do + post "/admin/themes/import.json", params: { theme: theme_file } + expect(response.status).to eq(201) + + json = ::JSON.parse(response.body) + + expect(json["theme"]["name"]).to eq("Sam's Simple Theme") + expect(json["theme"]["theme_fields"].length).to eq(2) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + end + + describe '#index' do + it 'correctly returns themes' do + ColorScheme.destroy_all + Theme.destroy_all + + theme = Theme.new(name: 'my name', user_id: -1) + theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') + theme.set_field(target: :desktop, name: :after_header, value: 'test') + + theme.remote_theme = RemoteTheme.new( + remote_url: 'awesome.git', + remote_version: '7', + local_version: '8', + remote_updated_at: Time.zone.now + ) + + theme.save! + + # this will get serialized as well + ColorScheme.create_from_base(name: "test", colors: []) + + get "/admin/themes.json" + + expect(response.status).to eq(200) + + json = ::JSON.parse(response.body) + + expect(json["extras"]["color_schemes"].length).to eq(2) + theme_json = json["themes"].find { |t| t["id"] == theme.id } + expect(theme_json["theme_fields"].length).to eq(2) + expect(theme_json["remote_theme"]["remote_version"]).to eq("7") + end + end + + describe '#create' do + it 'creates a theme' do + post "/admin/themes.json", params: { + theme: { + name: 'my test name', + theme_fields: [name: 'scss', target: 'common', value: 'body{color: red;}'] + } + } + + expect(response.status).to eq(201) + + json = ::JSON.parse(response.body) + + expect(json["theme"]["theme_fields"].length).to eq(1) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + end + + describe '#update' do + let(:theme) { Theme.create(name: 'my name', user_id: -1) } + + it 'can change default theme' do + SiteSetting.default_theme_key = nil + + put "/admin/themes/#{theme.id}.json", params: { + id: theme.id, theme: { default: true } + } + + expect(response.status).to eq(200) + expect(SiteSetting.default_theme_key).to eq(theme.key) + end + + it 'can unset default theme' do + SiteSetting.default_theme_key = theme.key + + put "/admin/themes/#{theme.id}.json", params: { + theme: { default: false } + } + + expect(response.status).to eq(200) + expect(SiteSetting.default_theme_key).to be_blank + end + + it 'updates a theme' do + theme.set_field(target: :common, name: :scss, value: '.body{color: black;}') + theme.save + + child_theme = Theme.create(name: 'my name', user_id: -1) + + upload = Fabricate(:upload) + + put "/admin/themes/#{theme.id}.json", params: { + theme: { + child_theme_ids: [child_theme.id], + name: 'my test name', + theme_fields: [ + { name: 'scss', target: 'common', value: '' }, + { name: 'scss', target: 'desktop', value: 'body{color: blue;}' }, + { name: 'bob', target: 'common', value: '', type_id: 2, upload_id: upload.id }, + ] + } + } + + expect(response.status).to eq(200) + + json = ::JSON.parse(response.body) + + fields = json["theme"]["theme_fields"].sort { |a, b| a["value"] <=> b["value"] } + + expect(fields[0]["value"]).to eq('') + expect(fields[0]["upload_id"]).to eq(upload.id) + expect(fields[1]["value"]).to eq('body{color: blue;}') + expect(fields.length).to eq(2) + expect(json["theme"]["child_themes"].length).to eq(1) + expect(UserHistory.where(action: UserHistory.actions[:change_theme]).count).to eq(1) + end + end +end diff --git a/spec/controllers/admin/user_fields_controller_spec.rb b/spec/requests/admin/user_fields_controller_spec.rb similarity index 69% rename from spec/controllers/admin/user_fields_controller_spec.rb rename to spec/requests/admin/user_fields_controller_spec.rb index de46a1cf0c..f388eeb419 100644 --- a/spec/controllers/admin/user_fields_controller_spec.rb +++ b/spec/requests/admin/user_fields_controller_spec.rb @@ -1,92 +1,93 @@ require 'rails_helper' describe Admin::UserFieldsController do - it "is a subclass of AdminController" do expect(Admin::UserFieldsController < Admin::AdminController).to eq(true) end context "when logged in" do - let!(:user) { log_in(:admin) } + let(:admin) { Fabricate(:admin) } - context '.create' do + before do + sign_in(admin) + end + + describe '#create' do it "creates a user field" do expect { - post :create, params: { + post "/admin/customize/user_fields.json", params: { user_field: { name: 'hello', description: 'hello desc', field_type: 'text' } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) }.to change(UserField, :count).by(1) end it "creates a user field with options" do expect do - post :create, params: { + post "/admin/customize/user_fields.json", params: { user_field: { name: 'hello', description: 'hello desc', field_type: 'dropdown', options: ['a', 'b', 'c'] } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) end.to change(UserField, :count).by(1) expect(UserFieldOption.count).to eq(3) end end - context '.index' do + describe '#index' do let!(:user_field) { Fabricate(:user_field) } it "returns a list of user fields" do - get :index, format: :json - expect(response).to be_success + get "/admin/customize/user_fields.json" + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json['user_fields']).to be_present end end - context '.destroy' do + describe '#destroy' do let!(:user_field) { Fabricate(:user_field) } it "deletes the user field" do expect { - delete :destroy, params: { id: user_field.id }, format: :json - expect(response).to be_success + delete "/admin/customize/user_fields/#{user_field.id}.json" + expect(response.status).to eq(200) }.to change(UserField, :count).by(-1) end end - context '.update' do + describe '#update' do let!(:user_field) { Fabricate(:user_field) } it "updates the user field" do - put :update, params: { - id: user_field.id, + put "/admin/customize/user_fields/#{user_field.id}.json", params: { user_field: { name: 'fraggle', field_type: 'confirm', description: 'muppet' } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) user_field.reload expect(user_field.name).to eq('fraggle') expect(user_field.field_type).to eq('confirm') end it "updates the user field options" do - put :update, params: { - id: user_field.id, + put "/admin/customize/user_fields/#{user_field.id}.json", params: { user_field: { name: 'fraggle', field_type: 'dropdown', description: 'muppet', options: ['hello', 'hello', 'world'] } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) user_field.reload expect(user_field.name).to eq('fraggle') expect(user_field.field_type).to eq('dropdown') @@ -94,8 +95,7 @@ describe Admin::UserFieldsController do end it "keeps options when updating the user field" do - put :update, params: { - id: user_field.id, + put "/admin/customize/user_fields/#{user_field.id}.json", params: { user_field: { name: 'fraggle', field_type: 'dropdown', @@ -103,27 +103,25 @@ describe Admin::UserFieldsController do options: ['hello', 'hello', 'world'], position: 1 } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) user_field.reload expect(user_field.user_field_options.size).to eq(2) - put :update, params: { - id: user_field.id, + put "/admin/customize/user_fields/#{user_field.id}.json", params: { user_field: { name: 'fraggle', field_type: 'dropdown', description: 'muppet', position: 2 } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) user_field.reload expect(user_field.user_field_options.size).to eq(2) end end end - end diff --git a/spec/requests/admin/users_controller_spec.rb b/spec/requests/admin/users_controller_spec.rb index a89e92f9cf..67cf0ee5b9 100644 --- a/spec/requests/admin/users_controller_spec.rb +++ b/spec/requests/admin/users_controller_spec.rb @@ -4,6 +4,808 @@ RSpec.describe Admin::UsersController do let(:admin) { Fabricate(:admin) } let(:user) { Fabricate(:user) } + it 'is a subclass of AdminController' do + expect(Admin::UsersController < Admin::AdminController).to eq(true) + end + + before do + sign_in(admin) + end + + describe '#index' do + it 'returns success with JSON' do + get "/admin/users/list.json" + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to be_present + end + + context 'when showing emails' do + it "returns email for all the users" do + get "/admin/users/list.json", params: { show_emails: "true" } + expect(response.status).to eq(200) + data = ::JSON.parse(response.body) + data.each do |user| + expect(user["email"]).to be_present + end + end + + it "logs only 1 enty" do + expect do + get "/admin/users/list.json", params: { show_emails: "true" } + end.to change { UserHistory.where(action: UserHistory.actions[:check_email], acting_user_id: admin.id).count }.by(1) + expect(response.status).to eq(200) + end + end + end + + describe '#show' do + context 'an existing user' do + it 'returns success' do + get "/admin/users/#{user.id}.json" + expect(response.status).to eq(200) + end + end + + context 'a non-existing user' do + it 'returns 404 error' do + get "/admin/users/0.json" + expect(response.status).to eq(404) + end + end + end + + describe '#approve' do + let(:evil_trout) { Fabricate(:evil_trout, approved: false) } + + it "raises an error when the user doesn't have permission" do + sign_in(user) + put "/admin/users/#{evil_trout.id}/approve.json" + expect(response.status).to eq(404) + evil_trout.reload + expect(evil_trout.approved).to eq(false) + end + + it 'calls approve' do + put "/admin/users/#{evil_trout.id}/approve.json" + expect(response.status).to eq(200) + evil_trout.reload + expect(evil_trout.approved).to eq(true) + end + end + + describe '#approve_bulk' do + let(:evil_trout) { Fabricate(:evil_trout, approved: false) } + + it "does nothing without uesrs" do + put "/admin/users/approve-bulk.json" + evil_trout.reload + expect(response.status).to eq(200) + expect(evil_trout.approved).to eq(false) + end + + it "won't approve the user when not allowed" do + sign_in(user) + put "/admin/users/approve-bulk.json", params: { users: [evil_trout.id] } + expect(response.status).to eq(404) + evil_trout.reload + expect(evil_trout.approved).to eq(false) + end + + it "approves the user when permitted" do + put "/admin/users/approve-bulk.json", params: { users: [evil_trout.id] } + expect(response.status).to eq(200) + evil_trout.reload + expect(evil_trout.approved).to eq(true) + end + end + + describe '#generate_api_key' do + it 'calls generate_api_key' do + post "/admin/users/#{user.id}/generate_api_key.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["api_key"]["user"]["id"]).to eq(user.id) + expect(json["api_key"]["key"]).to be_present + end + end + + describe '#revoke_api_key' do + it 'calls revoke_api_key' do + ApiKey.create!(user: user, key: SecureRandom.hex) + delete "/admin/users/#{user.id}/revoke_api_key.json" + expect(response.status).to eq(200) + expect(ApiKey.where(user: user).count).to eq(0) + end + end + + describe '#suspend' do + let(:post) { Fabricate(:post) } + let(:suspend_params) do + { suspend_until: 5.hours.from_now, + reason: "because of this post", + post_id: post.id } + end + + it "works properly" do + expect(user).not_to be_suspended + put "/admin/users/#{user.id}/suspend.json", params: { + suspend_until: 5.hours.from_now, + reason: "because I said so" + } + + expect(response.status).to eq(200) + + user.reload + expect(user).to be_suspended + expect(user.suspended_at).to be_present + expect(user.suspended_till).to be_present + + log = UserHistory.where(target_user_id: user.id).order('id desc').first + expect(log.details).to match(/because I said so/) + end + + context "with an associated post" do + it "can have an associated post" do + put "/admin/users/#{user.id}/suspend.json", params: suspend_params + + expect(response.status).to eq(200) + + log = UserHistory.where(target_user_id: user.id).order('id desc').first + expect(log.post_id).to eq(post.id) + end + + it "can delete an associated post" do + put "/admin/users/#{user.id}/suspend.json", params: suspend_params.merge(post_action: 'delete') + post.reload + expect(post.deleted_at).to be_present + expect(response.status).to eq(200) + end + + it "can edit an associated post" do + put "/admin/users/#{user.id}/suspend.json", params: suspend_params.merge( + post_action: 'edit', + post_edit: 'this is the edited content' + ) + + expect(response.status).to eq(200) + post.reload + expect(post.deleted_at).to be_blank + expect(post.raw).to eq("this is the edited content") + expect(response.status).to eq(200) + end + end + + it "can send a message to the user" do + put "/admin/users/#{user.id}/suspend.json", params: { + suspend_until: 10.days.from_now, + reason: "short reason", + message: "long reason" + } + + expect(response.status).to eq(200) + + expect(Jobs::CriticalUserEmail.jobs.size).to eq(1) + job_args = Jobs::CriticalUserEmail.jobs.first["args"].first + expect(job_args["type"]).to eq("account_suspended") + expect(job_args["user_id"]).to eq(user.id) + + log = UserHistory.where(target_user_id: user.id).order('id desc').first + expect(log).to be_present + expect(log.details).to match(/short reason/) + expect(log.details).to match(/long reason/) + end + + it "also revokes any api keys" do + Fabricate(:api_key, user: user) + put "/admin/users/#{user.id}/suspend.json", params: suspend_params + + expect(response.status).to eq(200) + user.reload + + expect(user).to be_suspended + expect(ApiKey.where(user_id: user.id).count).to eq(0) + end + end + + describe '#revoke_admin' do + let(:another_admin) { Fabricate(:admin) } + + it 'raises an error unless the user can revoke access' do + sign_in(user) + put "/admin/users/#{another_admin.id}/revoke_admin.json" + expect(response.status).to eq(404) + another_admin.reload + expect(another_admin.admin).to eq(true) + end + + it 'updates the admin flag' do + put "/admin/users/#{another_admin.id}/revoke_admin.json" + expect(response.status).to eq(200) + another_admin.reload + expect(another_admin.admin).to eq(false) + end + end + + describe '#grant_admin' do + let(:another_user) { Fabricate(:coding_horror) } + + after do + $redis.flushall + end + + it "raises an error when the user doesn't have permission" do + sign_in(user) + put "/admin/users/#{another_user.id}/grant_admin.json" + expect(response.status).to eq(404) + expect(AdminConfirmation.exists_for?(another_user.id)).to eq(false) + end + + it "returns a 403 if the username doesn't exist" do + put "/admin/users/123123/grant_admin.json" + expect(response.status).to eq(403) + end + + it 'updates the admin flag' do + expect(AdminConfirmation.exists_for?(another_user.id)).to eq(false) + put "/admin/users/#{another_user.id}/grant_admin.json" + expect(response.status).to eq(200) + expect(AdminConfirmation.exists_for?(another_user.id)).to eq(true) + end + end + + describe '#add_group' do + let(:group) { Fabricate(:group) } + + it 'adds the user to the group' do + post "/admin/users/#{user.id}/groups.json", params: { + group_id: group.id + } + + expect(response.status).to eq(200) + expect(GroupUser.where(user_id: user.id, group_id: group.id).exists?).to eq(true) + + group_history = GroupHistory.last + + expect(group_history.action).to eq(GroupHistory.actions[:add_user_to_group]) + expect(group_history.acting_user).to eq(admin) + expect(group_history.target_user).to eq(user) + + # Doing it again doesn't raise an error + post "/admin/users/#{user.id}/groups.json", params: { + group_id: group.id + } + + expect(response.status).to eq(200) + end + end + + describe '#remove_group' do + it "also clears the user's primary group" do + g = Fabricate(:group) + u = Fabricate(:user, primary_group: g) + delete "/admin/users/#{u.id}/groups/#{g.id}.json" + + expect(response.status).to eq(200) + expect(u.reload.primary_group).to eq(nil) + end + end + + describe '#trust_level' do + let(:another_user) { Fabricate(:coding_horror, created_at: 1.month.ago) } + + it "raises an error when the user doesn't have permission" do + sign_in(user) + put "/admin/users/#{another_user.id}/trust_level.json" + expect(response.status).to eq(404) + end + + it "returns a 422 if the username doesn't exist" do + put "/admin/users/123123/trust_level.json" + expect(response.status).to eq(422) + end + + it "upgrades the user's trust level" do + put "/admin/users/#{another_user.id}/trust_level.json", params: { level: 2 } + + expect(response.status).to eq(200) + another_user.reload + expect(another_user.trust_level).to eq(2) + + expect(UserHistory.where( + target_user: another_user, + acting_user: admin, + action: UserHistory.actions[:change_trust_level] + ).count).to eq(1) + end + + it "raises no error when demoting a user below their current trust level (locks trust level)" do + stat = another_user.user_stat + stat.topics_entered = SiteSetting.tl1_requires_topics_entered + 1 + stat.posts_read_count = SiteSetting.tl1_requires_read_posts + 1 + stat.time_read = SiteSetting.tl1_requires_time_spent_mins * 60 + stat.save! + another_user.update_attributes(trust_level: TrustLevel[1]) + + put "/admin/users/#{another_user.id}/trust_level.json", params: { + level: TrustLevel[0] + } + + expect(response.status).to eq(200) + another_user.reload + expect(another_user.trust_level).to eq(TrustLevel[0]) + expect(another_user.manual_locked_trust_level).to eq(TrustLevel[0]) + end + end + + describe '#grant_moderation' do + let(:another_user) { Fabricate(:coding_horror) } + + it "raises an error when the user doesn't have permission" do + sign_in(user) + put "/admin/users/#{another_user.id}/grant_moderation.json" + expect(response.status).to eq(404) + end + + it "returns a 403 if the username doesn't exist" do + put "/admin/users/123123/grant_moderation.json" + expect(response.status).to eq(403) + end + + it 'updates the moderator flag' do + put "/admin/users/#{another_user.id}/grant_moderation.json" + expect(response.status).to eq(200) + another_user.reload + expect(another_user.moderator).to eq(true) + end + end + + describe '#revoke_moderation' do + let(:moderator) { Fabricate(:moderator) } + + it 'raises an error unless the user can revoke access' do + sign_in(user) + put "/admin/users/#{moderator.id}/revoke_moderation.json" + expect(response.status).to eq(404) + moderator.reload + expect(moderator.moderator).to eq(true) + end + + it 'updates the moderator flag' do + put "/admin/users/#{moderator.id}/revoke_moderation.json" + expect(response.status).to eq(200) + moderator.reload + expect(moderator.moderator).to eq(false) + end + end + + describe '#primary_group' do + let(:group) { Fabricate(:group) } + let(:another_user) { Fabricate(:coding_horror) } + + it "raises an error when the user doesn't have permission" do + sign_in(user) + put "/admin/users/#{another_user.id}/primary_group.json" + expect(response.status).to eq(404) + another_user.reload + expect(another_user.primary_group_id).to eq(nil) + end + + it "returns a 404 if the user doesn't exist" do + put "/admin/users/123123/primary_group.json" + expect(response.status).to eq(403) + end + + it "changes the user's primary group" do + group.add(another_user) + put "/admin/users/#{another_user.id}/primary_group.json", params: { + primary_group_id: group.id + } + + expect(response.status).to eq(200) + another_user.reload + expect(another_user.primary_group_id).to eq(group.id) + end + + it "doesn't change primary group if they aren't a member of the group" do + put "/admin/users/#{another_user.id}/primary_group.json", params: { + primary_group_id: group.id + } + + expect(response.status).to eq(200) + another_user.reload + expect(another_user.primary_group_id).to eq(nil) + end + + it "remove user's primary group" do + group.add(another_user) + + put "/admin/users/#{another_user.id}/primary_group.json", params: { + primary_group_id: "" + } + + expect(response.status).to eq(200) + another_user.reload + expect(another_user.primary_group_id).to eq(nil) + end + end + + describe '#destroy' do + let(:delete_me) { Fabricate(:user) } + + it "returns a 403 if the user doesn't exist" do + delete "/admin/users/123123drink.json" + expect(response.status).to eq(403) + end + + context "user has post" do + let(:topic) { Fabricate(:topic, user: delete_me) } + let!(:post) { Fabricate(:post, topic: topic, user: delete_me) } + + it "returns an api response that the user can't be deleted because it has posts" do + delete "/admin/users/#{delete_me.id}.json" + expect(response.status).to eq(403) + json = ::JSON.parse(response.body) + expect(json['deleted']).to eq(false) + end + + it "doesn't return an error if delete_posts == true" do + delete "/admin/users/#{delete_me.id}.json", params: { delete_posts: true } + expect(response.status).to eq(200) + expect(Post.where(id: post.id).count).to eq(0) + expect(Topic.where(id: topic.id).count).to eq(0) + expect(User.where(id: delete_me.id).count).to eq(0) + end + end + + it "deletes the user record" do + delete "/admin/users/#{delete_me.id}.json" + expect(response.status).to eq(200) + expect(User.where(id: delete_me.id).count).to eq(0) + end + end + + describe '#activate' do + let(:reg_user) { Fabricate(:inactive_user) } + + it "returns success" do + put "/admin/users/#{reg_user.id}/activate.json" + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['success']).to eq("OK") + reg_user.reload + expect(reg_user.active).to eq(true) + end + + it "should confirm email even when the tokens are expired" do + reg_user.email_tokens.update_all(confirmed: false, expired: true) + + reg_user.reload + expect(reg_user.email_confirmed?).to eq(false) + + put "/admin/users/#{reg_user.id}/activate.json" + expect(response.status).to eq(200) + + reg_user.reload + expect(reg_user.email_confirmed?).to eq(true) + end + end + + describe '#log_out' do + let(:reg_user) { Fabricate(:user) } + + it "returns success" do + post "/admin/users/#{reg_user.id}/log_out.json" + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['success']).to eq("OK") + end + + it "returns 404 when user_id does not exist" do + post "/admin/users/123123drink/log_out.json" + expect(response.status).to eq(404) + end + end + + describe '#silence' do + let(:reg_user) { Fabricate(:user) } + + it "raises an error when the user doesn't have permission" do + sign_in(user) + put "/admin/users/#{reg_user.id}/silence.json" + expect(response.status).to eq(404) + reg_user.reload + expect(reg_user).not_to be_silenced + end + + it "returns a 403 if the user doesn't exist" do + put "/admin/users/123123/silence.json" + expect(response.status).to eq(403) + end + + it "punishes the user for spamming" do + put "/admin/users/#{reg_user.id}/silence.json" + expect(response.status).to eq(200) + reg_user.reload + expect(reg_user).to be_silenced + end + + it "can have an associated post" do + silence_post = Fabricate(:post, user: reg_user) + + put "/admin/users/#{reg_user.id}/silence.json", params: { + post_id: silence_post.id, + post_action: 'edit', + post_edit: "this is the new contents for the post" + } + expect(response.status).to eq(200) + + silence_post.reload + expect(silence_post.raw).to eq("this is the new contents for the post") + + log = UserHistory.where( + target_user_id: reg_user.id, + action: UserHistory.actions[:silence_user] + ).first + expect(log).to be_present + expect(log.post_id).to eq(silence_post.id) + + reg_user.reload + expect(reg_user).to be_silenced + end + + it "will set a length of time if provided" do + future_date = 1.month.from_now.to_date + put "/admin/users/#{reg_user.id}/silence.json", params: { + silenced_till: future_date + } + + expect(response.status).to eq(200) + reg_user.reload + expect(reg_user).to be_silenced + expect(reg_user.silenced_till).to eq(future_date) + end + + it "will send a message if provided" do + expect do + put "/admin/users/#{reg_user.id}/silence.json", params: { + message: "Email this to the user" + } + end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1) + + expect(response.status).to eq(200) + reg_user.reload + expect(reg_user).to be_silenced + end + end + + describe '#unsilence' do + let(:reg_user) { Fabricate(:user, silenced_till: 10.years.from_now) } + + it "raises an error when the user doesn't have permission" do + sign_in(user) + put "/admin/users/#{reg_user.id}/unsilence.json" + expect(response.status).to eq(404) + end + + it "returns a 403 if the user doesn't exist" do + put "/admin/users/123123/unsilence.json" + expect(response.status).to eq(403) + end + + it "unsilences the user" do + put "/admin/users/#{reg_user.id}/unsilence.json" + expect(response.status).to eq(200) + reg_user.reload + expect(reg_user.silenced?).to eq(false) + log = UserHistory.where( + target_user_id: reg_user.id, + action: UserHistory.actions[:unsilence_user] + ).first + expect(log).to be_present + end + end + + describe '#reject_bulk' do + let(:reject_me) { Fabricate(:user) } + let(:reject_me_too) { Fabricate(:user) } + + it 'does nothing without users' do + delete "/admin/users/reject-bulk.json" + expect(response.status).to eq(200) + expect(User.where(id: reject_me.id).count).to eq(1) + expect(User.where(id: reject_me_too.id).count).to eq(1) + end + + it "won't delete users if not allowed" do + sign_in(user) + delete "/admin/users/reject-bulk.json", params: { + users: [reject_me.id] + } + expect(response.status).to eq(404) + expect(User.where(id: reject_me.id).count).to eq(1) + end + + it "reports successes" do + delete "/admin/users/reject-bulk.json", params: { + users: [reject_me.id, reject_me_too.id] + } + + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['success'].to_i).to eq(2) + expect(json['failed'].to_i).to eq(0) + expect(User.where(id: reject_me.id).count).to eq(0) + expect(User.where(id: reject_me_too.id).count).to eq(0) + end + + context 'failures' do + it 'can handle some successes and some failures' do + stat = reject_me_too.user_stat + stat.first_post_created_at = (SiteSetting.delete_user_max_post_age.to_i + 1).days.ago + stat.post_count = 10 + stat.save! + + delete "/admin/users/reject-bulk.json", params: { + users: [reject_me.id, reject_me_too.id] + } + + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['success'].to_i).to eq(1) + expect(json['failed'].to_i).to eq(1) + expect(User.where(id: reject_me.id).count).to eq(0) + expect(User.where(id: reject_me_too.id).count).to eq(1) + end + + it 'reports failure due to a user still having posts' do + Fabricate(:post, user: reject_me) + + delete "/admin/users/reject-bulk.json", params: { + users: [reject_me.id] + } + + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json['success'].to_i).to eq(0) + expect(json['failed'].to_i).to eq(1) + expect(User.where(id: reject_me.id).count).to eq(1) + end + end + end + + describe '#ip_info' do + it "uses ipinfo.io webservice to retrieve the info" do + ip = "192.168.1.1" + ip_data = { + city: "Jeddah", + country: "SA", + ip: ip + } + url = "https://ipinfo.io/#{ip}/json" + + stub_request(:get, url).to_return(status: 200, body: ip_data.to_json) + get "/admin/users/ip-info.json", params: { ip: ip } + expect(response.status).to eq(200) + expect(JSON.parse(response.body).symbolize_keys).to eq(ip_data) + end + end + + describe '#delete_other_accounts_with_same_ip' do + it "works" do + user_a = Fabricate(:user, ip_address: "42.42.42.42") + user_b = Fabricate(:user, ip_address: "42.42.42.42") + + delete "/admin/users/delete-others-with-same-ip.json", params: { + ip: "42.42.42.42", exclude: -1, order: "trust_level DESC" + } + expect(response.status).to eq(200) + expect(User.where(id: user_a.id).count).to eq(0) + expect(User.where(id: user_b.id).count).to eq(0) + end + end + + describe '#invite_admin' do + let(:api_key) { Fabricate(:api_key, user: admin, key: SecureRandom.hex) } + let(:api_params) do + { api_key: api_key.key, api_username: admin.username } + end + + it "doesn't work when not via API" do + post "/admin/users/invite_admin.json", params: { + name: 'Bill', username: 'bill22', email: 'bill@bill.com' + } + + expect(response.status).to eq(403) + end + + it 'should invite admin' do + expect do + post "/admin/users/invite_admin.json", params: api_params.merge( + name: 'Bill', username: 'bill22', email: 'bill@bill.com' + ) + end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1) + + expect(response.status).to eq(200) + + u = User.find_by_email('bill@bill.com') + expect(u.name).to eq("Bill") + expect(u.username).to eq("bill22") + expect(u.admin).to eq(true) + end + + it "doesn't send the email with send_email falsey" do + expect do + post "/admin/users/invite_admin.json", params: api_params.merge( + name: 'Bill', username: 'bill22', email: 'bill@bill.com', send_email: '0' + ) + end.to change { Jobs::CriticalUserEmail.jobs.size }.by(0) + + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["password_url"]).to be_present + end + end + + describe '#sync_sso' do + let(:sso) { SingleSignOn.new } + let(:sso_secret) { "sso secret" } + + before do + SiteSetting.email_editable = false + SiteSetting.sso_url = "https://www.example.com/sso" + SiteSetting.enable_sso = true + SiteSetting.sso_overrides_email = true + SiteSetting.sso_overrides_name = true + SiteSetting.sso_overrides_username = true + SiteSetting.sso_secret = sso_secret + sso.sso_secret = sso_secret + end + + it 'can sync up with the sso' do + sso.name = "Bob The Bob" + sso.username = "bob" + sso.email = "bob@bob.com" + sso.external_id = "1" + + user = DiscourseSingleSignOn.parse(sso.payload).lookup_or_create_user + + sso.name = "Bill" + sso.username = "Hokli$$!!" + sso.email = "bob2@bob.com" + + post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) + expect(response.status).to eq(200) + + user.reload + expect(user.email).to eq("bob2@bob.com") + expect(user.name).to eq("Bill") + expect(user.username).to eq("Hokli") + end + + it 'should create new users' do + sso.name = "Dr. Claw" + sso.username = "dr_claw" + sso.email = "dr@claw.com" + sso.external_id = "2" + post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) + expect(response.status).to eq(200) + + user = User.find_by_email('dr@claw.com') + expect(user).to be_present + expect(user.ip_address).to be_blank + end + + it 'should return the right message if the record is invalid' do + sso.email = "" + sso.name = "" + sso.external_id = "1" + + post "/admin/users/sync_sso.json", params: Rack::Utils.parse_query(sso.payload) + expect(response.status).to eq(403) + expect(JSON.parse(response.body)["message"]).to include("Primary email can't be blank") + end + end + describe '#disable_second_factor' do let(:second_factor) { user.create_totp } diff --git a/spec/controllers/admin/versions_controller_spec.rb b/spec/requests/admin/versions_controller_spec.rb similarity index 73% rename from spec/controllers/admin/versions_controller_spec.rb rename to spec/requests/admin/versions_controller_spec.rb index 62c8cea39d..f8ef30aa50 100644 --- a/spec/controllers/admin/versions_controller_spec.rb +++ b/spec/requests/admin/versions_controller_spec.rb @@ -15,21 +15,23 @@ describe Admin::VersionsController do end context 'while logged in as an admin' do + let(:admin) { Fabricate(:admin) } before do - @user = log_in(:admin) + sign_in(admin) end describe 'show' do - subject { get :show, format: :json } - it { is_expected.to be_success } - it 'should return the currently available version' do - json = JSON.parse(subject.body) + get "/admin/version_check.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) expect(json['latest_version']).to eq('1.2.33') end it "should return the installed version" do - json = JSON.parse(subject.body) + get "/admin/version_check.json" + json = JSON.parse(response.body) + expect(response.status).to eq(200) expect(json['installed_version']).to eq(Discourse::VERSION::STRING) end end diff --git a/spec/controllers/admin/web_hooks_controller_spec.rb b/spec/requests/admin/web_hooks_controller_spec.rb similarity index 65% rename from spec/controllers/admin/web_hooks_controller_spec.rb rename to spec/requests/admin/web_hooks_controller_spec.rb index 6b793dd0de..eed31ce051 100644 --- a/spec/controllers/admin/web_hooks_controller_spec.rb +++ b/spec/requests/admin/web_hooks_controller_spec.rb @@ -7,14 +7,16 @@ describe Admin::WebHooksController do end context 'while logged in as an admin' do - before do - @user = log_in(:admin) - end let(:web_hook) { Fabricate(:web_hook) } + let(:admin) { Fabricate(:admin) } + + before do + sign_in(admin) + end describe '#create' do it 'creates a webhook' do - post :create, params: { + post "/admin/api/web_hooks.json", params: { web_hook: { payload_url: 'https://meta.discourse.org/', content_type: 1, @@ -26,16 +28,16 @@ describe Admin::WebHooksController do group_ids: [], category_ids: [] } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) - expect(json["web_hook"]["payload_url"]).to be_present + expect(json["web_hook"]["payload_url"]).to eq("https://meta.discourse.org/") end it 'returns error when field is not filled correctly' do - post :create, params: { + post "/admin/api/web_hooks.json", params: { web_hook: { content_type: 1, secret: "a_secret_for_webhooks", @@ -46,9 +48,9 @@ describe Admin::WebHooksController do group_ids: [], category_ids: [] } - }, format: :json + } - expect(response.status).to eq 422 + expect(response.status).to eq(422) response_body = JSON.parse(response.body) expect(response_body["errors"]).to be_present @@ -57,12 +59,14 @@ describe Admin::WebHooksController do describe '#ping' do it 'enqueues the ping event' do - Jobs.expects(:enqueue) - .with(:emit_web_hook_event, web_hook_id: web_hook.id, event_type: 'ping', event_name: 'ping') + expect do + post "/admin/api/web_hooks/#{web_hook.id}/ping.json" + end.to change { Jobs::EmitWebHookEvent.jobs.size }.by(1) - post :ping, params: { id: web_hook.id }, format: :json - - expect(response).to be_success + expect(response.status).to eq(200) + job_args = Jobs::EmitWebHookEvent.jobs.first["args"].first + expect(job_args["web_hook_id"]).to eq(web_hook.id) + expect(job_args["event_type"]).to eq("ping") end end end diff --git a/spec/requests/categories_controller_spec.rb b/spec/requests/categories_controller_spec.rb index 9f0b113aec..e3a76cacbf 100644 --- a/spec/requests/categories_controller_spec.rb +++ b/spec/requests/categories_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe CategoriesController do + let(:admin) { Fabricate(:admin) } + let!(:category) { Fabricate(:category, user: admin) } context 'index' do @@ -18,11 +20,7 @@ describe CategoriesController do end context 'extensibility event' do - let(:admin) { Fabricate(:admin) } - let(:category) { Fabricate(:category, user: admin) } - before do - category sign_in(admin) end @@ -39,4 +37,314 @@ describe CategoriesController do expect(event[:params].first).to eq(category) end end + + context '#create' do + it "requires the user to be logged in" do + post "/categories.json" + expect(response.status).to eq(403) + end + + describe "logged in" do + before do + SiteSetting.queue_jobs = false + sign_in(admin) + end + + it "raises an exception when they don't have permission to create it" do + sign_in(Fabricate(:user)) + post "/categories.json", params: { + name: 'hello', color: 'ff0', text_color: 'fff' + } + + expect(response).to be_forbidden + end + + it "raises an exception when the name is missing" do + post "/categories.json", params: { color: "ff0", text_color: "fff" } + expect(response.status).to eq(400) + end + + it "raises an exception when the color is missing" do + post "/categories.json", params: { name: "hello", text_color: "fff" } + expect(response.status).to eq(400) + end + + it "raises an exception when the text color is missing" do + post "/categories.json", params: { name: "hello", color: "ff0" } + end + + describe "failure" do + it "returns errors on a duplicate category name" do + category = Fabricate(:category, user: admin) + + post "/categories.json", params: { + name: category.name, color: "ff0", text_color: "fff" + } + + expect(response.status).to eq(422) + end + end + + describe "success" do + it "works" do + readonly = CategoryGroup.permission_types[:readonly] + create_post = CategoryGroup.permission_types[:create_post] + + post "/categories.json", params: { + name: "hello", + color: "ff0", + text_color: "fff", + slug: "hello-cat", + auto_close_hours: 72, + permissions: { + "everyone" => readonly, + "staff" => create_post + } + } + + expect(response.status).to eq(200) + category = Category.find_by(name: "hello") + expect(category.category_groups.map { |g| [g.group_id, g.permission_type] }.sort).to eq([ + [Group[:everyone].id, readonly], [Group[:staff].id, create_post] + ]) + expect(category.name).to eq("hello") + expect(category.slug).to eq("hello-cat") + expect(category.color).to eq("ff0") + expect(category.auto_close_hours).to eq(72) + expect(UserHistory.count).to eq(4) # 1 + 3 (bootstrap mode) + end + end + end + end + + context '#destroy' do + it "requires the user to be logged in" do + delete "/categories/category.json" + expect(response.status).to eq(403) + end + + describe "logged in" do + it "raises an exception if they don't have permission to delete it" do + admin.update!(admin: false) + sign_in(admin) + delete "/categories/#{category.slug}.json" + expect(response).to be_forbidden + end + + it "deletes the record" do + sign_in(admin) + expect do + delete "/categories/#{category.slug}.json" + end.to change(Category, :count).by(-1) + expect(response.status).to eq(200) + expect(UserHistory.count).to eq(1) + end + end + end + + context '#reorder' do + it "reorders the categories" do + sign_in(admin) + + c1 = category + c2 = Fabricate(:category) + c3 = Fabricate(:category) + c4 = Fabricate(:category) + if c3.id < c2.id + tmp = c3; c2 = c3; c3 = tmp; + end + c1.position = 8 + c2.position = 6 + c3.position = 7 + c4.position = 5 + + payload = {} + payload[c1.id] = 4 + payload[c2.id] = 6 + payload[c3.id] = 6 + payload[c4.id] = 5 + + post "/categories/reorder.json", params: { mapping: MultiJson.dump(payload) } + + SiteSetting.fixed_category_positions = true + list = CategoryList.new(Guardian.new(admin)) + + expect(list.categories).to eq([ + Category.find(SiteSetting.uncategorized_category_id), + c1, + c4, + c2, + c3 + ]) + end + end + + context '#update' do + before do + SiteSetting.queue_jobs = false + end + + it "requires the user to be logged in" do + put "/categories/category.json" + expect(response.status).to eq(403) + end + + describe "logged in" do + let(:valid_attrs) { { id: category.id, name: "hello", color: "ff0", text_color: "fff" } } + + before do + sign_in(admin) + end + + it "raises an exception if they don't have permission to edit it" do + sign_in(Fabricate(:user)) + put "/categories/#{category.slug}.json", params: { + name: 'hello', + color: 'ff0', + text_color: 'fff' + } + expect(response).to be_forbidden + end + + it "requires a name" do + put "/categories/#{category.slug}.json", params: { + color: 'fff', + text_color: '0ff', + } + expect(response.status).to eq(400) + end + + it "requires a color" do + put "/categories/#{category.slug}.json", params: { + name: 'asdf', + text_color: '0ff', + } + expect(response.status).to eq(400) + end + + it "requires a text color" do + put "/categories/#{category.slug}.json", params: { name: 'asdf', color: 'fff' } + expect(response.status).to eq(400) + end + + it "returns errors on a duplicate category name" do + other_category = Fabricate(:category, name: "Other", user: admin) + put "/categories/#{category.id}.json", params: { + name: other_category.name, + color: "ff0", + text_color: "fff", + } + expect(response.status).to eq(422) + end + + it "returns 422 if email_in address is already in use for other category" do + other_category = Fabricate(:category, name: "Other", email_in: "mail@examle.com") + + put "/categories/#{category.id}.json", params: { + name: "Email", + email_in: "mail@examle.com", + color: "ff0", + text_color: "fff", + } + expect(response.status).to eq(422) + end + + describe "success" do + it "updates the group correctly" do + readonly = CategoryGroup.permission_types[:readonly] + create_post = CategoryGroup.permission_types[:create_post] + + put "/categories/#{category.id}.json", params: { + name: "hello", + color: "ff0", + text_color: "fff", + slug: "hello-category", + auto_close_hours: 72, + permissions: { + "everyone" => readonly, + "staff" => create_post + }, + custom_fields: { + "dancing" => "frogs" + }, + } + + expect(response.status).to eq(200) + category.reload + expect(category.category_groups.map { |g| [g.group_id, g.permission_type] }.sort).to eq([ + [Group[:everyone].id, readonly], [Group[:staff].id, create_post] + ]) + expect(category.name).to eq("hello") + expect(category.slug).to eq("hello-category") + expect(category.color).to eq("ff0") + expect(category.auto_close_hours).to eq(72) + expect(category.custom_fields).to eq("dancing" => "frogs") + end + + it 'logs the changes correctly' do + category.update!(permissions: { "admins" => CategoryGroup.permission_types[:create_post] }) + + put "/categories/#{category.id}.json", params: { + name: 'new name', + color: category.color, + text_color: category.text_color, + slug: category.slug, + permissions: { + "everyone" => CategoryGroup.permission_types[:create_post] + }, + } + expect(response.status).to eq(200) + expect(UserHistory.count).to eq(5) # 2 + 3 (bootstrap mode) + end + end + end + end + + context '#update_slug' do + it 'requires the user to be logged in' do + put "/category/category/slug.json" + expect(response.status).to eq(403) + end + + describe 'logged in' do + let(:valid_attrs) { { id: category.id, slug: 'fff' } } + + before do + sign_in(admin) + end + + it 'rejects blank' do + put "/category/#{category.id}/slug.json", params: { slug: nil } + expect(response.status).to eq(422) + end + + it 'accepts valid custom slug' do + put "/category/#{category.id}/slug.json", params: { slug: 'valid-slug' } + + expect(response.status).to eq(200) + expect(category.reload.slug).to eq('valid-slug') + end + + it 'accepts not well formed custom slug' do + put "/category/#{category.id}/slug.json", params: { slug: ' valid slug' } + + expect(response.status).to eq(200) + expect(category.reload.slug).to eq('valid-slug') + end + + it 'accepts and sanitize custom slug when the slug generation method is not ascii' do + SiteSetting.slug_generation_method = 'none' + put "/category/#{category.id}/slug.json", params: { slug: ' another !_ slug @' } + + expect(response.status).to eq(200) + expect(category.reload.slug).to eq('another-slug') + SiteSetting.slug_generation_method = 'ascii' + end + + it 'rejects invalid custom slug' do + put "/category/#{category.id}/slug.json", params: { slug: ' ' } + expect(response.status).to eq(422) + end + end + end end diff --git a/spec/controllers/category_hashtags_controller_spec.rb b/spec/requests/category_hashtags_controller_spec.rb similarity index 66% rename from spec/controllers/category_hashtags_controller_spec.rb rename to spec/requests/category_hashtags_controller_spec.rb index f177811df2..c638d82ede 100644 --- a/spec/controllers/category_hashtags_controller_spec.rb +++ b/spec/requests/category_hashtags_controller_spec.rb @@ -4,14 +4,15 @@ describe CategoryHashtagsController do describe "check" do describe "logged in" do before do - log_in(:user) + sign_in(Fabricate(:user)) end it 'only returns the categories that are valid' do category = Fabricate(:category) - get :check, params: { category_slugs: [category.slug, 'none'] }, format: :json + get "/category_hashtags/check.json", params: { category_slugs: [category.slug, 'none'] } + expect(response.status).to eq(200) expect(JSON.parse(response.body)).to eq( "valid" => [{ "slug" => category.hashtag_slug, "url" => category.url_with_id }] ) @@ -21,21 +22,22 @@ describe CategoryHashtagsController do group = Fabricate(:group) private_category = Fabricate(:private_category, group: group) - get :check, params: { category_slugs: [private_category.slug] }, format: :json + get "/category_hashtags/check.json", params: { category_slugs: [private_category.slug] } + expect(response.status).to eq(200) expect(JSON.parse(response.body)).to eq("valid" => []) end it 'returns restricted categories for an admin' do - admin = log_in(:admin) + admin = sign_in(Fabricate(:admin)) group = Fabricate(:group) group.add(admin) private_category = Fabricate(:private_category, group: group) - get :check, - params: { category_slugs: [private_category.slug] }, - format: :json + get "/category_hashtags/check.json", + params: { category_slugs: [private_category.slug] } + expect(response.status).to eq(200) expect(JSON.parse(response.body)).to eq( "valid" => [{ "slug" => private_category.hashtag_slug, "url" => private_category.url_with_id }] ) @@ -44,7 +46,7 @@ describe CategoryHashtagsController do describe "not logged in" do it 'raises an exception' do - get :check, params: { category_slugs: [] }, format: :json + get "/category_hashtags/check.json", params: { category_slugs: [] } expect(response.status).to eq(403) end end diff --git a/spec/requests/clicks_controller_spec.rb b/spec/requests/clicks_controller_spec.rb new file mode 100644 index 0000000000..293b07bdf7 --- /dev/null +++ b/spec/requests/clicks_controller_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +describe ClicksController do + context 'create' do + context 'missing params' do + it 'raises a 404 without a url' do + get "/clicks/track", params: { post_id: 123 } + expect(response).to be_not_found + end + end + + context 'correct params' do + let(:url) { "https://discourse.org/" } + let(:headers) { { REMOTE_ADDR: "192.168.0.1" } } + let(:post) { create_post(raw: "this is a post with a link #{url}") } + + context "with a made up url" do + it "doesn't redirect" do + get "/clicks/track", params: { url: 'https://fakewebsite.com', post_id: post.id }, headers: headers + expect(response).not_to be_redirect + expect(response.body).to include(I18n.t("redirect_warning")) + end + end + + context "with a valid url" do + it "redirects" do + get "/clicks/track", params: { url: 'https://discourse.org/?hello=123', post_id: post.id }, headers: headers + expect(response).to redirect_to("https://discourse.org/?hello=123") + end + end + + context 'with a post_id' do + it 'redirects' do + get "/clicks/track", params: { url: url, post_id: post.id }, headers: headers + expect(response).to redirect_to(url) + end + + it "redirects links in whispers to staff members" do + sign_in(Fabricate(:admin)) + whisper = Fabricate(:post, post_type: Post.types[:whisper]) + + get "/clicks/track", params: { url: url, post_id: whisper.id }, headers: headers + + expect(response).to redirect_to(url) + end + + it "doesn't redirect with the redirect=false param" do + get "/clicks/track", params: { url: url, post_id: post.id, redirect: 'false' }, headers: headers + expect(response).not_to be_redirect + end + end + + context 'with a topic_id' do + it 'redirects' do + get "/clicks/track", params: { url: url, topic_id: post.topic.id }, headers: headers + expect(response).to redirect_to(url) + end + end + end + end +end diff --git a/spec/requests/composer_messages_controller_spec.rb b/spec/requests/composer_messages_controller_spec.rb new file mode 100644 index 0000000000..ea3163ccfa --- /dev/null +++ b/spec/requests/composer_messages_controller_spec.rb @@ -0,0 +1,33 @@ +require 'rails_helper' + +describe ComposerMessagesController do + let(:topic) { Fabricate(:topic, created_at: 10.years.ago, last_posted_at: 10.years.ago) } + let(:post) { Fabricate(:post, topic: topic, post_number: 1, created_at: 10.years.ago) } + + context '#index' do + it 'requires you to be logged in' do + get "/composer_messages.json" + expect(response.status).to eq(403) + end + + context 'when logged in' do + let!(:user) { sign_in(Fabricate(:user)) } + let(:args) { { 'topic_id' => post.topic.id, 'post_id' => '333', 'composer_action' => 'reply' } } + + it 'redirects to your user preferences' do + get "/composer_messages.json" + expect(response.status).to eq(200) + end + + it 'delegates args to the finder' do + user.user_stat.update!(post_count: 10) + SiteSetting.disable_avatar_education_message = true + + get "/composer_messages.json", params: args + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["composer_messages"].first["id"]).to eq("reviving_old") + end + end + end +end diff --git a/spec/requests/directory_items_controller_spec.rb b/spec/requests/directory_items_controller_spec.rb index c912634703..8a188db107 100644 --- a/spec/requests/directory_items_controller_spec.rb +++ b/spec/requests/directory_items_controller_spec.rb @@ -14,7 +14,7 @@ describe DirectoryItemsController do it "requires a proper `period` param" do get '/directory_items.json', params: { period: 'eviltrout' } - expect(response).not_to be_success + expect(response).not_to be_successful end context "without data" do @@ -24,7 +24,7 @@ describe DirectoryItemsController do it "succeeds" do get '/directory_items.json', params: { period: 'all' } - expect(response).to be_success + expect(response.status).to eq(200) end end @@ -37,7 +37,7 @@ describe DirectoryItemsController do it "succeeds with a valid value" do get '/directory_items.json', params: { period: 'all' } - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present @@ -53,12 +53,12 @@ describe DirectoryItemsController do SiteSetting.enable_user_directory = false get '/directory_items.json', params: { period: 'all' } - expect(response).not_to be_success + expect(response).not_to be_successful end it "finds user by name" do get '/directory_items.json', params: { period: 'all', name: 'eviltrout' } - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present @@ -69,7 +69,7 @@ describe DirectoryItemsController do it "finds staged user by name" do get '/directory_items.json', params: { period: 'all', name: 'stage_user' } - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present @@ -80,7 +80,7 @@ describe DirectoryItemsController do it "excludes users by username" do get '/directory_items.json', params: { period: 'all', exclude_usernames: "stage_user,eviltrout" } - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present @@ -92,7 +92,7 @@ describe DirectoryItemsController do it "filters users by group" do get '/directory_items.json', params: { period: 'all', group: group.name } - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present diff --git a/spec/controllers/draft_controller_spec.rb b/spec/requests/draft_controller_spec.rb similarity index 53% rename from spec/controllers/draft_controller_spec.rb rename to spec/requests/draft_controller_spec.rb index d7e63615f2..55f471a44d 100644 --- a/spec/controllers/draft_controller_spec.rb +++ b/spec/requests/draft_controller_spec.rb @@ -1,23 +1,23 @@ require 'rails_helper' describe DraftController do - it 'requires you to be logged in' do - post :update + post "/draft" expect(response.status).to eq(403) end it 'saves a draft on update' do - user = log_in - post :update, params: { draft_key: 'xyz', data: 'my data', sequence: 0 }, format: :json + user = sign_in(Fabricate(:user)) + post "/draft.json", params: { draft_key: 'xyz', data: 'my data', sequence: 0 } + expect(response.status).to eq(200) expect(Draft.get(user, 'xyz', 0)).to eq('my data') end it 'destroys drafts when required' do - user = log_in + user = sign_in(Fabricate(:user)) Draft.set(user, 'xxx', 0, 'hi') - delete :destroy, params: { draft_key: 'xxx', sequence: 0 }, format: :json + delete "/draft.json", params: { draft_key: 'xxx', sequence: 0 } + expect(response.status).to eq(200) expect(Draft.get(user, 'xxx', 0)).to eq(nil) end - end diff --git a/spec/requests/email_controller_spec.rb b/spec/requests/email_controller_spec.rb index 196b3b277a..3b6a1a8f30 100644 --- a/spec/requests/email_controller_spec.rb +++ b/spec/requests/email_controller_spec.rb @@ -145,7 +145,7 @@ RSpec.describe EmailController do key = SecureRandom.hex $redis.set(key, user.email) get '/email/unsubscribed', params: { key: key, topic_id: topic.id } - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(topic.title) end end @@ -155,9 +155,122 @@ RSpec.describe EmailController do key = SecureRandom.hex $redis.set(key, user.email) get '/email/unsubscribed', params: { key: key, topic_id: private_topic.id } - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to_not include(private_topic.title) end end end + + context '#preferences_redirect' do + it 'requires you to be logged in' do + get "/email_preferences.json" + expect(response.status).to eq(403) + end + + context 'when logged in' do + let!(:user) { sign_in(Fabricate(:user)) } + + it 'redirects to your user preferences' do + get "/email_preferences.json" + expect(response).to redirect_to("/u/#{user.username}/preferences") + end + end + end + + context '#unsubscribe' do + it 'displays log out button if wrong user logged in' do + sign_in(Fabricate(:admin)) + user = Fabricate(:user) + key = UnsubscribeKey.create_key_for(user, "digest") + + get "/email/unsubscribe/#{key}" + + expect(response.body).to include(I18n.t("unsubscribe.log_out")) + expect(response.body).to include(I18n.t("unsubscribe.different_user_description")) + end + + it 'displays not found if key is not found' do + get "/email/unsubscribe/#{SecureRandom.hex}" + expect(response.body).to include(CGI.escapeHTML(I18n.t("unsubscribe.not_found_description"))) + end + + it 'correctly handles mailing list mode' do + user = Fabricate(:user) + key = UnsubscribeKey.create_key_for(user, "digest") + + user.user_option.update_columns(mailing_list_mode: true) + + get "/email/unsubscribe/#{key}" + expect(response.body).to include(I18n.t("unsubscribe.mailing_list_mode")) + + SiteSetting.disable_mailing_list_mode = true + + get "/email/unsubscribe/#{key}" + expect(response.body).not_to include(I18n.t("unsubscribe.mailing_list_mode")) + + user.user_option.update_columns(mailing_list_mode: false) + SiteSetting.disable_mailing_list_mode = false + + get "/email/unsubscribe/#{key}" + expect(response.body).not_to include(I18n.t("unsubscribe.mailing_list_mode")) + end + + it 'correctly handles digest unsubscribe' do + user = Fabricate(:user) + user.user_option.update_columns(email_digests: false) + key = UnsubscribeKey.create_key_for(user, "digest") + + # because we are type digest we will always show digest and it will be selected + get "/email/unsubscribe/#{key}" + expect(response.body).to include(I18n.t("unsubscribe.disable_digest_emails")) + + source = Nokogiri::HTML::fragment(response.body) + expect(source.css("#disable_digest_emails")[0]["checked"]).to eq("checked") + + SiteSetting.disable_digest_emails = true + + get "/email/unsubscribe/#{key}" + expect(response.body).not_to include(I18n.t("unsubscribe.disable_digest_emails")) + + SiteSetting.disable_digest_emails = false + key = UnsubscribeKey.create_key_for(user, "not_digest") + + get "/email/unsubscribe/#{key}" + expect(response.body).to include(I18n.t("unsubscribe.disable_digest_emails")) + end + + it 'correctly handles watched categories' do + post = Fabricate(:post) + user = post.user + cu = CategoryUser.create!(user_id: user.id, + category_id: post.topic.category_id, + notification_level: CategoryUser.notification_levels[:watching]) + + key = UnsubscribeKey.create_key_for(user, post) + get "/email/unsubscribe/#{key}" + expect(response.body).to include("unwatch_category") + + cu.destroy! + + get "/email/unsubscribe/#{key}" + expect(response.body).not_to include("unwatch_category") + end + + it 'correctly handles watched first post categories' do + post = Fabricate(:post) + user = post.user + cu = CategoryUser.create!(user_id: user.id, + category_id: post.topic.category_id, + notification_level: CategoryUser.notification_levels[:watching_first_post]) + + key = UnsubscribeKey.create_key_for(user, post) + get "/email/unsubscribe/#{key}" + expect(response.body).to include("unwatch_category") + + cu.destroy! + + get "/email/unsubscribe/#{key}" + expect(response.body).not_to include("unwatch_category") + end + end end diff --git a/spec/requests/embed_controller_spec.rb b/spec/requests/embed_controller_spec.rb index 42b6f018d3..f7f5c11eee 100644 --- a/spec/requests/embed_controller_spec.rb +++ b/spec/requests/embed_controller_spec.rb @@ -27,7 +27,7 @@ describe EmbedController do it "allows a topic to be embedded by id" do topic = Fabricate(:topic) get '/embed/comments', params: { topic_id: topic.id }, headers: headers - expect(response).to be_success + expect(response.status).to eq(200) end end @@ -71,21 +71,45 @@ describe EmbedController do context "with a host" do let!(:embeddable_host) { Fabricate(:embeddable_host) } + let(:headers) { { 'REFERER' => embed_url } } + + before do + SiteSetting.queue_jobs = false + end it "raises an error with no referer" do get '/embed/comments', params: { embed_url: embed_url } expect(response.body).to match(I18n.t('embed.error')) end + it "includes CSS from embedded_scss field" do + theme = Theme.create!(name: "Awesome blog", user_id: -1) + theme.set_default! + + ThemeField.create!( + theme_id: theme.id, + name: "embedded_scss", + target_id: 0, + type_id: 1, + value: ".test-osama-15 {\n" + " color: red;\n" + "}\n" + ) + + topic_embed = Fabricate(:topic_embed, embed_url: embed_url) + post = Fabricate(:post, topic: topic_embed.topic) + + get '/embed/comments', params: { embed_url: embed_url }, headers: headers + + html = Nokogiri::HTML.fragment(response.body) + css_link = html.at("link[data-target=embedded_theme]").attribute("href").value + + get css_link + expect(response.status).to eq(200) + expect(response.body).to include(".test-osama-15") + end + context "success" do - let(:headers) { { 'REFERER' => embed_url } } - - before do - SiteSetting.queue_jobs = false - end - after do - expect(response).to be_success + expect(response.status).to eq(200) expect(response.headers['X-Frame-Options']).to eq("ALLOWALL") end @@ -139,7 +163,7 @@ describe EmbedController do params: { embed_url: embed_url }, headers: { 'REFERER' => "http://eviltrout.com/wat/1-2-3.html" } - expect(response).to be_success + expect(response.status).to eq(200) end it "works with the second host" do @@ -147,7 +171,7 @@ describe EmbedController do params: { embed_url: embed_url }, headers: { 'REFERER' => "http://eviltrout.com/wat/1-2-3.html" } - expect(response).to be_success + expect(response.status).to eq(200) end it "works with a host with a path" do @@ -155,7 +179,7 @@ describe EmbedController do params: { embed_url: embed_url }, headers: { 'REFERER' => "https://example.com/some-other-path" } - expect(response).to be_success + expect(response.status).to eq(200) end it "contains custom class name" do diff --git a/spec/requests/export_csv_controller_spec.rb b/spec/requests/export_csv_controller_spec.rb new file mode 100644 index 0000000000..1fe2a15e7c --- /dev/null +++ b/spec/requests/export_csv_controller_spec.rb @@ -0,0 +1,63 @@ +require "rails_helper" + +describe ExportCsvController do + let(:export_filename) { "user-archive-codinghorror-150115-234817-999.csv.gz" } + + context "while logged in as normal user" do + let(:user) { Fabricate(:user) } + before { sign_in(user) } + + describe ".export_entity" do + it "enqueues export job" do + post "/export_csv/export_entity.json", params: { entity: "user_archive" } + expect(response.status).to eq(200) + expect(Jobs::ExportCsvFile.jobs.size).to eq(1) + + job_data = Jobs::ExportCsvFile.jobs.first["args"].first + expect(job_data["entity"]).to eq("user_archive") + expect(job_data["user_id"]).to eq(user.id) + end + + it "should not enqueue export job if rate limit is reached" do + UserExport.create(file_name: "user-archive-codinghorror-150116-003249", user_id: user.id) + post "/export_csv/export_entity.json", params: { entity: "user_archive" } + expect(response).to be_forbidden + expect(Jobs::ExportCsvFile.jobs.size).to eq(0) + end + + it "returns 404 when normal user tries to export admin entity" do + post "/export_csv/export_entity.json", params: { entity: "staff_action" } + expect(response).to be_forbidden + expect(Jobs::ExportCsvFile.jobs.size).to eq(0) + end + end + end + + context "while logged in as an admin" do + let(:admin) { Fabricate(:admin) } + before { sign_in(admin) } + + describe ".export_entity" do + it "enqueues export job" do + post "/export_csv/export_entity.json", params: { entity: "staff_action" } + expect(response.status).to eq(200) + expect(Jobs::ExportCsvFile.jobs.size).to eq(1) + + job_data = Jobs::ExportCsvFile.jobs.first["args"].first + expect(job_data["entity"]).to eq("staff_action") + expect(job_data["user_id"]).to eq(admin.id) + end + + it "should not rate limit export for staff" do + UserExport.create(file_name: "screened-email-150116-010145", user_id: admin.id) + post "/export_csv/export_entity.json", params: { entity: "staff_action" } + expect(response.status).to eq(200) + expect(Jobs::ExportCsvFile.jobs.size).to eq(1) + + job_data = Jobs::ExportCsvFile.jobs.first["args"].first + expect(job_data["entity"]).to eq("staff_action") + expect(job_data["user_id"]).to eq(admin.id) + end + end + end +end diff --git a/spec/controllers/extra_locales_controller_spec.rb b/spec/requests/extra_locales_controller_spec.rb similarity index 69% rename from spec/controllers/extra_locales_controller_spec.rb rename to spec/requests/extra_locales_controller_spec.rb index c3aa308d51..a190489f4f 100644 --- a/spec/controllers/extra_locales_controller_spec.rb +++ b/spec/requests/extra_locales_controller_spec.rb @@ -1,28 +1,27 @@ require 'rails_helper' describe ExtraLocalesController do - context 'show' do - it "caches for 24 hours if version is provided and it matches current hash" do - get :show, params: { bundle: 'admin', v: ExtraLocalesController.bundle_js_hash('admin') } + get "/extra-locales/admin", params: { v: ExtraLocalesController.bundle_js_hash('admin') } + expect(response.status).to eq(200) expect(response.headers["Cache-Control"]).to eq("max-age=86400, public, immutable") end it "does not cache at all if version is invalid" do - get :show, params: { bundle: 'admin', v: 'a' * 32 } + get "/extra-locales/admin", params: { v: 'a' * 32 } + expect(response.status).to eq(200) expect(response.headers["Cache-Control"]).not_to eq("max-age=86400, public, immutable") end it "needs a valid bundle" do - get :show, params: { bundle: 'made-up-bundle' } - expect(response).to_not be_success - expect(response.body).to be_blank + get "/extra-locales/made-up-bundle" + expect(response.status).to eq(403) end it "won't work with a weird parameter" do - get :show, params: { bundle: '-invalid..character!!' } - expect(response).to_not be_success + get "/extra-locales/-invalid..character!!" + expect(response.status).to eq(404) end it "includes plugin translations" do @@ -41,12 +40,10 @@ describe ExtraLocalesController do } }).at_least_once - get :show, params: { bundle: "admin" } + get "/extra-locales/admin" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body.include?("github_badges")).to eq(true) end - end - end diff --git a/spec/controllers/finish_installation_controller_spec.rb b/spec/requests/finish_installation_controller_spec.rb similarity index 62% rename from spec/controllers/finish_installation_controller_spec.rb rename to spec/requests/finish_installation_controller_spec.rb index e239a9af30..0703a2d9c0 100644 --- a/spec/controllers/finish_installation_controller_spec.rb +++ b/spec/requests/finish_installation_controller_spec.rb @@ -2,15 +2,15 @@ require 'rails_helper' describe FinishInstallationController do - describe '.index' do + describe '#index' do context "has_login_hint is false" do before do SiteSetting.has_login_hint = false end it "doesn't allow access" do - get :index - expect(response).not_to be_success + get "/finish-installation" + expect(response).to be_forbidden end end @@ -20,21 +20,21 @@ describe FinishInstallationController do end it "allows access" do - get :index - expect(response).to be_success + get "/finish-installation" + expect(response.status).to eq(200) end end end - describe '.register' do + describe '#register' do context "has_login_hint is false" do before do SiteSetting.has_login_hint = false end it "doesn't allow access" do - get :register - expect(response).not_to be_success + get "/finish-installation/register" + expect(response).to be_forbidden end end @@ -45,21 +45,21 @@ describe FinishInstallationController do end it "allows access" do - get :register - expect(response).to be_success + get "/finish-installation/register" + expect(response.status).to eq(200) end it "raises an error when the email is not in the allowed list" do - post :register, params: { + post "/finish-installation/register.json", params: { email: 'notrobin@example.com', username: 'eviltrout', password: 'disismypasswordokay' - }, format: :json + } expect(response.status).to eq(400) end it "doesn't redirect when fields are wrong" do - post :register, params: { + post "/finish-installation/register", params: { email: 'robin@example.com', username: '', password: 'disismypasswordokay' @@ -69,40 +69,39 @@ describe FinishInstallationController do end it "registers the admin when the email is in the list" do - Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup)) - - post :register, params: { - email: 'robin@example.com', - username: 'eviltrout', - password: 'disismypasswordokay' - }, format: :json + expect do + post "/finish-installation/register.json", params: { + email: 'robin@example.com', + username: 'eviltrout', + password: 'disismypasswordokay' + } + end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1) expect(response).to be_redirect expect(User.where(username: 'eviltrout').exists?).to eq(true) end - end end - describe '.confirm_email' do + describe '#confirm_email' do context "has_login_hint is false" do before do SiteSetting.has_login_hint = false end it "shows the page" do - get :confirm_email - expect(response).to be_success + get "/finish-installation/confirm-email" + expect(response.status).to eq(200) end end end - describe '.resend_email' do + describe '#resend_email' do before do SiteSetting.has_login_hint = true GlobalSetting.stubs(:developer_emails).returns("robin@example.com") - post :register, params: { + post "/finish-installation/register", params: { email: 'robin@example.com', username: 'eviltrout', password: 'disismypasswordokay' @@ -110,9 +109,11 @@ describe FinishInstallationController do end it "resends the email" do - Jobs.expects(:enqueue).with(:critical_user_email, has_entries(type: :signup)) - get :resend_email - expect(response).to be_success + expect do + put "/finish-installation/resend-email" + end.to change { Jobs::CriticalUserEmail.jobs.size }.by(1) + + expect(response.status).to eq(200) end end end diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index b268542391..cae3ae4c7f 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -152,7 +152,7 @@ describe GroupsController do staff_group get "/groups.json" - expect(response).to be_success + expect(response.status).to eq(200) response_body = JSON.parse(response.body) @@ -192,7 +192,7 @@ describe GroupsController do describe 'owner groups' do it 'should return the right response' do group2 = Fabricate(:group) - group3 = Fabricate(:group) + _group3 = Fabricate(:group) group2.add_owner(admin) expect_type_to_return_right_groups('owner', [group.id, group2.id]) @@ -219,7 +219,7 @@ describe GroupsController do describe 'close groups' do it 'should return the right response' do group2 = Fabricate(:group, public_admission: false) - group3 = Fabricate(:group, public_admission: true) + _group3 = Fabricate(:group, public_admission: true) expect_type_to_return_right_groups('close', [group.id, group2.id]) end @@ -380,25 +380,49 @@ describe GroupsController do describe '#mentionable' do it "should return the right response" do sign_in(user) - group.update_attributes!(name: 'test') - get "/groups/test/mentionable.json", params: { name: group.name } - - expect(response).to be_success + get "/groups/#{group.name}/mentionable.json" + expect(response.status).to eq(200) response_body = JSON.parse(response.body) expect(response_body["mentionable"]).to eq(false) - group.update_attributes!(mentionable_level: Group::ALIAS_LEVELS[:everyone]) + group.update_attributes!( + mentionable_level: Group::ALIAS_LEVELS[:everyone], + visibility_level: Group.visibility_levels[:staff] + ) - get "/groups/test/mentionable.json", params: { name: group.name } - expect(response).to be_success + get "/groups/#{group.name}/mentionable.json" + expect(response.status).to eq(200) response_body = JSON.parse(response.body) expect(response_body["mentionable"]).to eq(true) end end + describe '#messageable' do + it "should return the right response" do + sign_in(user) + + get "/groups/#{group.name}/messageable.json" + expect(response.status).to eq(200) + + response_body = JSON.parse(response.body) + expect(response_body["messageable"]).to eq(false) + + group.update!( + messageable_level: Group::ALIAS_LEVELS[:everyone], + visibility_level: Group.visibility_levels[:staff] + ) + + get "/groups/#{group.name}/messageable.json" + expect(response.status).to eq(200) + + response_body = JSON.parse(response.body) + expect(response_body["messageable"]).to eq(true) + end + end + describe '#update' do let(:group) do Fabricate(:group, @@ -621,7 +645,7 @@ describe GroupsController do order: 'last_seen_at', desc: true } - expect(response).to be_success + expect(response.status).to eq(200) members = JSON.parse(response.body)["members"] @@ -629,7 +653,7 @@ describe GroupsController do get "/groups/#{group.name}/members.json", params: { order: 'last_seen_at' } - expect(response).to be_success + expect(response.status).to eq(200) members = JSON.parse(response.body)["members"] @@ -639,7 +663,7 @@ describe GroupsController do order: 'last_posted_at', desc: true } - expect(response).to be_success + expect(response.status).to eq(200) members = JSON.parse(response.body)["members"] @@ -649,7 +673,7 @@ describe GroupsController do it "should not allow members to be sorted by columns that are not allowed" do get "/groups/#{group.name}/members.json", params: { order: 'email' } - expect(response).to be_success + expect(response.status).to eq(200) members = JSON.parse(response.body)["members"] @@ -784,7 +808,7 @@ describe GroupsController do put "/groups/#{group.id}/members.json", params: { usernames: user2.username } end.to change { group.users.count }.by(1) - expect(response).to be_success + expect(response.status).to eq(200) group_history = GroupHistory.last @@ -810,7 +834,7 @@ describe GroupsController do params: { usernames: [user1.username, user2.username].join(",") } end.to change { group.users.count }.by(2) - expect(response).to be_success + expect(response.status).to eq(200) end it "adds by id" do @@ -819,7 +843,7 @@ describe GroupsController do params: { user_ids: [user1.id, user2.id].join(",") } end.to change { group.users.count }.by(2) - expect(response).to be_success + expect(response.status).to eq(200) end it "adds by email" do @@ -828,7 +852,7 @@ describe GroupsController do params: { user_emails: [user1.email, user2.email].join(",") } end.to change { group.users.count }.by(2) - expect(response).to be_success + expect(response.status).to eq(200) end it 'fails when multiple member already exists' do @@ -896,7 +920,7 @@ describe GroupsController do params: { usernames: other_user.username } end.to change { group.users.count }.by(1) - expect(response).to be_success + expect(response.status).to eq(200) group_history = GroupHistory.last @@ -914,7 +938,7 @@ describe GroupsController do params: { usernames: other_user.username } end.to change { group.users.count }.by(1) - expect(response).to be_success + expect(response.status).to eq(200) end it 'should not allow an underprivilege user to add another user to a group' do @@ -947,7 +971,7 @@ describe GroupsController do delete "/groups/#{group.id}/members.json", params: { user_id: user.id } end.to change { group.users.count }.by(-1) - expect(response).to be_success + expect(response.status).to eq(200) end it "removes by username" do @@ -955,7 +979,7 @@ describe GroupsController do delete "/groups/#{group.id}/members.json", params: { username: user.username } end.to change { group.users.count }.by(-1) - expect(response).to be_success + expect(response.status).to eq(200) end it "removes user.primary_group_id when user is removed from group" do @@ -972,7 +996,7 @@ describe GroupsController do params: { user_email: user.email } end.to change { group.users.count }.by(-1) - expect(response).to be_success + expect(response.status).to eq(200) end context 'public group' do @@ -986,7 +1010,7 @@ describe GroupsController do params: { username: other_user.username } end.to change { group.users.count }.by(-1) - expect(response).to be_success + expect(response.status).to eq(200) end end @@ -998,7 +1022,7 @@ describe GroupsController do params: { username: other_user.username } end.to change { group.users.count }.by(-1) - expect(response).to be_success + expect(response.status).to eq(200) end it 'should not allow a underprivilege user to leave a group for another user' do @@ -1053,7 +1077,7 @@ describe GroupsController do it 'should allow group owner to view history' do get "/groups/#{group.name}/logs.json" - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body)["logs"].last @@ -1085,7 +1109,7 @@ describe GroupsController do get "/groups/#{group.name}/logs.json" - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body)["logs"].first @@ -1108,7 +1132,7 @@ describe GroupsController do filters: { "action" => "add_user_to_group" } } - expect(response).to be_success + expect(response.status).to eq(200) logs = JSON.parse(response.body)["logs"] @@ -1143,7 +1167,7 @@ describe GroupsController do post "/groups/#{group.name}/request_membership.json", params: { reason: 'Please add me in' } - expect(response).to be_success + expect(response.status).to eq(200) post = Post.last topic = post.topic @@ -1193,7 +1217,7 @@ describe GroupsController do get '/groups/search.json' - expect(response).to be_success + expect(response.status).to eq(200) groups = JSON.parse(response.body) expected_ids = Group::AUTO_GROUPS.map { |name, id| id } @@ -1205,7 +1229,7 @@ describe GroupsController do ['GO', 'nerys'].each do |term| get "/groups/search.json?term=#{term}" - expect(response).to be_success + expect(response.status).to eq(200) groups = JSON.parse(response.body) expect(groups.length).to eq(1) @@ -1214,7 +1238,7 @@ describe GroupsController do get "/groups/search.json?term=KingOfTheNorth" - expect(response).to be_success + expect(response.status).to eq(200) groups = JSON.parse(response.body) expect(groups).to eq([]) @@ -1231,7 +1255,7 @@ describe GroupsController do get "/groups/search.json?term=north" - expect(response).to be_success + expect(response.status).to eq(200) groups = JSON.parse(response.body) expect(groups.length).to eq(1) @@ -1245,7 +1269,7 @@ describe GroupsController do get '/groups/search.json?ignore_automatic=true' - expect(response).to be_success + expect(response.status).to eq(200) groups = JSON.parse(response.body) expect(groups.length).to eq(2) diff --git a/spec/controllers/inline_onebox_controller_spec.rb b/spec/requests/inline_onebox_controller_spec.rb similarity index 69% rename from spec/controllers/inline_onebox_controller_spec.rb rename to spec/requests/inline_onebox_controller_spec.rb index e68387fa55..c46835c82c 100644 --- a/spec/controllers/inline_onebox_controller_spec.rb +++ b/spec/requests/inline_onebox_controller_spec.rb @@ -3,16 +3,16 @@ require 'rails_helper' describe InlineOneboxController do it "requires the user to be logged in" do - get :show, params: { urls: [] }, format: :json + get "/inline-onebox.json", params: { urls: [] } expect(response.status).to eq(403) end context "logged in" do - let!(:user) { log_in(:user) } + let!(:user) { sign_in(Fabricate(:user)) } it "returns empty JSON for empty input" do - get :show, params: { urls: [] }, format: :json - expect(response).to be_success + get "/inline-onebox.json", params: { urls: [] } + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json['inline-oneboxes']).to eq([]) end @@ -21,8 +21,8 @@ describe InlineOneboxController do let(:topic) { Fabricate(:topic) } it "returns information for a valid link" do - get :show, params: { urls: [ topic.url ] }, format: :json - expect(response).to be_success + get "/inline-onebox.json", params: { urls: [ topic.url ] } + expect(response.status).to eq(200) json = JSON.parse(response.body) onebox = json['inline-oneboxes'][0] @@ -31,7 +31,5 @@ describe InlineOneboxController do expect(onebox['title']).to eq(topic.title) end end - end - end diff --git a/spec/requests/invites_controller_spec.rb b/spec/requests/invites_controller_spec.rb index 4f622c196a..4b4586ce13 100644 --- a/spec/requests/invites_controller_spec.rb +++ b/spec/requests/invites_controller_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' describe InvitesController do - context 'show' do let(:invite) { Fabricate(:invite) } let(:user) { Fabricate(:coding_horror) } @@ -9,7 +8,7 @@ describe InvitesController do it "returns error if invite not found" do get "/invites/nopeNOPEnope" - expect(response).to be_success + expect(response.status).to eq(200) body = response.body expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' }) @@ -19,7 +18,7 @@ describe InvitesController do it "renders the accept invite page if invite exists" do get "/invites/#{invite.invite_key}" - expect(response).to be_success + expect(response.status).to eq(200) body = response.body expect(body).to have_tag(:script, with: { src: '/assets/application.js' }) @@ -30,11 +29,392 @@ describe InvitesController do invite.update_attributes!(redeemed_at: 1.day.ago) get "/invites/#{invite.invite_key}" - expect(response).to be_success + expect(response.status).to eq(200) body = response.body expect(body).to_not have_tag(:script, with: { src: '/assets/application.js' }) expect(CGI.unescapeHTML(body)).to include(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url)) end end + + context '#destroy' do + it 'requires you to be logged in' do + delete "/invites.json", + params: { email: 'jake@adventuretime.ooo' } + expect(response.status).to eq(403) + end + + context 'while logged in' do + let!(:user) { sign_in(Fabricate(:user)) } + let!(:invite) { Fabricate(:invite, invited_by: user) } + let(:another_invite) { Fabricate(:invite, email: 'anotheremail@address.com') } + + it 'raises an error when the email is missing' do + delete "/invites.json" + expect(response.status).to eq(400) + end + + it "raises an error when the email cannot be found" do + delete "/invites.json", params: { email: 'finn@adventuretime.ooo' } + expect(response.status).to eq(400) + end + + it 'raises an error when the invite is not yours' do + delete "/invites.json", params: { email: another_invite.email } + expect(response.status).to eq(400) + end + + it "destroys the invite" do + delete "/invites.json", params: { email: invite.email } + invite.reload + expect(invite.trashed?).to be_truthy + end + end + end + + context '#create' do + it 'requires you to be logged in' do + post "/invites.json", params: { email: 'jake@adventuretime.ooo' } + expect(response.status).to eq(403) + end + + context 'while logged in' do + let(:email) { 'jake@adventuretime.ooo' } + + it "fails if you can't invite to the forum" do + sign_in(Fabricate(:user)) + post "/invites.json", params: { email: email } + expect(response).to be_forbidden + end + + it "fails for normal user if invite email already exists" do + user = sign_in(Fabricate(:trust_level_4)) + invite = Invite.invite_by_email("invite@example.com", user) + post "/invites.json", params: { email: invite.email } + expect(response.status).to eq(422) + json = JSON.parse(response.body) + expect(json["failed"]).to be_present + end + + it "allows admins to invite to groups" do + group = Fabricate(:group) + sign_in(Fabricate(:admin)) + post "/invites.json", params: { email: email, group_names: group.name } + expect(response.status).to eq(200) + expect(Invite.find_by(email: email).invited_groups.count).to eq(1) + end + + it 'allows group owners to invite to groups' do + group = Fabricate(:group) + user = sign_in(Fabricate(:user)) + user.update!(trust_level: TrustLevel[2]) + group.add_owner(user) + + post "/invites.json", params: { email: email, group_names: group.name } + + expect(response.status).to eq(200) + expect(Invite.find_by(email: email).invited_groups.count).to eq(1) + end + + it "allows admin to send multiple invites to same email" do + user = sign_in(Fabricate(:admin)) + invite = Invite.invite_by_email("invite@example.com", user) + post "/invites.json", params: { email: invite.email } + expect(response.status).to eq(200) + end + + it "responds with error message in case of validation failure" do + sign_in(Fabricate(:admin)) + post "/invites.json", params: { email: "test@mailinator.com" } + expect(response.status).to eq(422) + json = JSON.parse(response.body) + expect(json["errors"]).to be_present + end + end + end + + context '#create_invite_link' do + it 'requires you to be logged in' do + post "/invites/link.json", params: { + email: 'jake@adventuretime.ooo' + } + expect(response.status).to eq(403) + end + + context 'while logged in' do + let(:email) { 'jake@adventuretime.ooo' } + + it "fails if you can't invite to the forum" do + sign_in(Fabricate(:user)) + post "/invites/link.json", params: { email: email } + expect(response).to be_forbidden + end + + it "fails for normal user if invite email already exists" do + user = sign_in(Fabricate(:trust_level_4)) + invite = Invite.invite_by_email("invite@example.com", user) + + post "/invites/link.json", params: { + email: invite.email + } + + expect(response.status).to eq(422) + end + + it "verifies that inviter is authorized to invite new user to a group-private topic" do + group = Fabricate(:group) + private_category = Fabricate(:private_category, group: group) + group_private_topic = Fabricate(:topic, category: private_category) + sign_in(Fabricate(:trust_level_4)) + + post "/invites/link.json", params: { + email: email, topic_id: group_private_topic.id + } + + expect(response).to be_forbidden + end + + it "allows admins to invite to groups" do + group = Fabricate(:group) + sign_in(Fabricate(:admin)) + + post "/invites/link.json", params: { + email: email, group_names: group.name + } + + expect(response.status).to eq(200) + expect(Invite.find_by(email: email).invited_groups.count).to eq(1) + end + + it "allows multiple group invite" do + Fabricate(:group, name: "security") + Fabricate(:group, name: "support") + sign_in(Fabricate(:admin)) + + post "/invites/link.json", params: { + email: email, group_names: "security,support" + } + + expect(response.status).to eq(200) + expect(Invite.find_by(email: email).invited_groups.count).to eq(2) + end + end + end + + context '#perform_accept_invitation' do + context 'with an invalid invite id' do + it "redirects to the root and doesn't change the session" do + put "/invites/show/doesntexist.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["success"]).to eq(false) + expect(json["message"]).to eq(I18n.t('invite.not_found')) + expect(session[:current_user_id]).to be_blank + end + end + + context 'with a deleted invite' do + let(:topic) { Fabricate(:topic) } + let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + before do + invite.destroy + end + + it "redirects to the root" do + put "/invites/show/#{invite.invite_key}.json" + + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["success"]).to eq(false) + expect(json["message"]).to eq(I18n.t('invite.not_found')) + expect(session[:current_user_id]).to be_blank + end + end + + context 'with a valid invite id' do + let(:topic) { Fabricate(:topic) } + let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + + it 'redeems the invite' do + put "/invites/show/#{invite.invite_key}.json" + invite.reload + expect(invite.redeemed?).to be_truthy + end + + context 'when redeem returns a user' do + let(:user) { Fabricate(:coding_horror) } + + context 'success' do + it 'logs in the user' do + events = DiscourseEvent.track_events do + put "/invites/show/#{invite.invite_key}.json" + end + + expect(events.map { |event| event[:event_name] }).to include( + :user_logged_in, :user_first_logged_in + ) + invite.reload + expect(response.status).to eq(200) + expect(session[:current_user_id]).to eq(invite.user_id) + expect(invite.redeemed?).to be_truthy + end + + it 'redirects to the first topic the user was invited to' do + put "/invites/show/#{invite.invite_key}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["success"]).to eq(true) + expect(json["redirect_to"]).to eq(topic.relative_url) + end + end + + context 'failure' do + it "doesn't log in the user if there's a validation error" do + put "/invites/show/#{invite.invite_key}.json", params: { password: "password" } + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["success"]).to eq(false) + expect(json["errors"]["password"]).to be_present + end + end + + context '.post_process_invite' do + before do + SiteSetting.queue_jobs = true + end + + it 'sends a welcome message if set' do + user.send_welcome_message = true + put "/invites/show/#{invite.invite_key}.json" + expect(response.status).to eq(200) + expect(Jobs::SendSystemMessage.jobs.size).to eq(1) + end + + it "sends password reset email if password is not set" do + put "/invites/show/#{invite.invite_key}.json" + expect(response.status).to eq(200) + expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(1) + end + + it "does not send password reset email if sso is enabled" do + SiteSetting.sso_url = "https://www.example.com/sso" + SiteSetting.enable_sso = true + put "/invites/show/#{invite.invite_key}.json" + expect(response.status).to eq(200) + expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0) + end + + it "does not send password reset email if local login is disabled" do + SiteSetting.enable_local_logins = false + put "/invites/show/#{invite.invite_key}.json" + expect(response.status).to eq(200) + expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0) + end + + it 'sends an activation email if password is set' do + put "/invites/show/#{invite.invite_key}.json", params: { password: "verystrongpassword" } + expect(response.status).to eq(200) + expect(Jobs::InvitePasswordInstructionsEmail.jobs.size).to eq(0) + expect(Jobs::CriticalUserEmail.jobs.size).to eq(1) + end + end + end + end + + context 'new registrations are disabled' do + let(:topic) { Fabricate(:topic) } + let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + before { SiteSetting.allow_new_registrations = false } + + it "doesn't redeem the invite" do + put "/invites/show/#{invite.invite_key}.json" + expect(response.status).to eq(200) + invite.reload + expect(invite.user_id).to be_blank + expect(invite.redeemed?).to be_falsey + expect(response.body).to include(I18n.t("login.new_registrations_disabled")) + end + end + + context 'user is already logged in' do + let(:topic) { Fabricate(:topic) } + let(:invite) { topic.invite_by_email(topic.user, "iceking@adventuretime.ooo") } + let!(:user) { sign_in(Fabricate(:user)) } + + it "doesn't redeem the invite" do + put "/invites/show/#{invite.invite_key}.json", params: { id: invite.invite_key } + expect(response.status).to eq(200) + invite.reload + expect(invite.user_id).to be_blank + expect(invite.redeemed?).to be_falsey + expect(response.body).to include(I18n.t("login.already_logged_in", current_user: user.username)) + end + end + end + + context '#resend_invite' do + it 'requires you to be logged in' do + post "/invites/reinvite.json", params: { email: 'first_name@example.com' } + expect(response.status).to eq(403) + end + + context 'while logged in' do + let!(:user) { sign_in(Fabricate(:user)) } + let!(:invite) { Fabricate(:invite, invited_by: user) } + let(:another_invite) { Fabricate(:invite, email: 'last_name@example.com') } + + it 'raises an error when the email is missing' do + post "/invites/reinvite.json" + expect(response.status).to eq(400) + end + + it "raises an error when the email cannot be found" do + post "/invites/reinvite.json", params: { email: 'first_name@example.com' } + expect(response.status).to eq(400) + end + + it 'raises an error when the invite is not yours' do + post "/invites/reinvite.json", params: { email: another_invite.email } + expect(response.status).to eq(400) + end + + it "resends the invite" do + SiteSetting.queue_jobs = true + post "/invites/reinvite.json", params: { email: invite.email } + expect(response.status).to eq(200) + expect(Jobs::InviteEmail.jobs.size).to eq(1) + end + end + end + + context '#upload_csv' do + it 'requires you to be logged in' do + post "/invites/upload_csv.json" + expect(response.status).to eq(403) + end + + context 'while logged in' do + let(:csv_file) { File.new("#{Rails.root}/spec/fixtures/csv/discourse.csv") } + + let(:file) do + Rack::Test::UploadedFile.new(File.open(csv_file)) + end + + let(:filename) { 'discourse.csv' } + + it "fails if you can't bulk invite to the forum" do + sign_in(Fabricate(:user)) + post "/invites/upload_csv.json", params: { file: file, name: filename } + expect(response.status).to eq(403) + end + + it "allows admin to bulk invite" do + SiteSetting.queue_jobs = true + sign_in(Fabricate(:admin)) + post "/invites/upload_csv.json", params: { file: file, name: filename } + expect(response.status).to eq(200) + expect(Jobs::BulkInvite.jobs.size).to eq(1) + end + end + end end diff --git a/spec/requests/list_controller_spec.rb b/spec/requests/list_controller_spec.rb index 288d49cd29..0f72cedfa0 100644 --- a/spec/requests/list_controller_spec.rb +++ b/spec/requests/list_controller_spec.rb @@ -1,18 +1,42 @@ require 'rails_helper' RSpec.describe ListController do - let(:topic) { Fabricate(:topic) } + let(:topic) { Fabricate(:topic, user: user) } let(:group) { Fabricate(:group) } + let(:user) { Fabricate(:user) } + let(:post) { Fabricate(:post, user: user) } + + before do + SiteSetting.top_menu = 'latest|new|unread|categories' + end describe '#index' do it "doesn't throw an error with a negative page" do get "/#{Discourse.anonymous_filters[1]}", params: { page: -1024 } - expect(response).to be_success + expect(response.status).to eq(200) end it "doesn't throw an error with page params as an array" do get "/#{Discourse.anonymous_filters[1]}", params: { page: ['7'] } - expect(response).to be_success + expect(response.status).to eq(200) + end + + (Discourse.anonymous_filters - [:categories]).each do |filter| + context "#{filter}" do + it "succeeds" do + get "/#{filter}" + expect(response.status).to eq(200) + end + end + end + + it 'allows users to filter on a set of topic ids' do + p = create_post + + get "/latest.json", params: { topic_ids: "#{p.topic_id}" } + expect(response.status).to eq(200) + parsed = JSON.parse(response.body) + expect(parsed["topic_list"]["topics"].length).to eq(1) end end @@ -210,4 +234,346 @@ RSpec.describe ListController do end end end + + describe 'RSS feeds' do + it 'renders latest RSS' do + get "/latest.rss" + expect(response.status).to eq(200) + expect(response.content_type).to eq('application/rss+xml') + end + + it 'renders top RSS' do + get "/top.rss" + expect(response.status).to eq(200) + expect(response.content_type).to eq('application/rss+xml') + end + + TopTopic.periods.each do |period| + it "renders #{period} top RSS" do + get "/top/#{period}.rss" + expect(response.status).to eq(200) + expect(response.content_type).to eq('application/rss+xml') + end + end + end + + describe 'category' do + context 'in a category' do + let(:category) { Fabricate(:category) } + let(:group) { Fabricate(:group) } + let(:private_category) { Fabricate(:private_category, group: group) } + + context 'without access to see the category' do + it "responds with a 404 error" do + get "/c/#{private_category.slug}/l/latest" + expect(response.status).to eq(404) + end + end + + context 'with access to see the category' do + it "succeeds" do + get "/c/#{category.slug}/l/latest" + expect(response.status).to eq(200) + end + end + + context 'with a link that includes an id' do + it "succeeds" do + get "/c/#{category.id}-#{category.slug}/l/latest" + expect(response.status).to eq(200) + end + end + + context 'with a link that has a parent slug, slug and id in its path' do + let(:child_category) { Fabricate(:category, parent_category: category) } + + context "with valid slug" do + it "redirects to the child category" do + get "/c/#{category.slug}/#{child_category.slug}/l/latest", params: { + id: child_category.id + } + expect(response).to redirect_to(child_category.url) + end + end + + context "with invalid slug" do + it "redirects to child category" do + get "/c/random_slug/another_random_slug/l/latest", params: { + id: child_category.id + } + expect(response).to redirect_to(child_category.url) + end + end + end + + context 'another category exists with a number at the beginning of its name' do + # One category has another category's id at the beginning of its name + let!(:other_category) { Fabricate(:category, name: "#{category.id} name") } + + it 'uses the correct category' do + get "/c/#{other_category.slug}/l/latest.json" + expect(response.status).to eq(200) + body = JSON.parse(response.body) + expect(body["topic_list"]["topics"].first["category_id"]) + .to eq(other_category.id) + end + end + + context 'a child category' do + let(:sub_category) { Fabricate(:category, parent_category_id: category.id) } + + context 'when parent and child are requested' do + it "succeeds" do + get "/c/#{category.slug}/#{sub_category.slug}/l/latest" + expect(response.status).to eq(200) + end + end + + context 'when child is requested with the wrong parent' do + it "responds with a 404 error" do + get "/c/not-the-right-slug/#{sub_category.slug}/l/latest" + expect(response.status).to eq(404) + end + end + end + + describe 'feed' do + it 'renders RSS' do + get "/c/#{category.slug}.rss" + expect(response.status).to eq(200) + expect(response.content_type).to eq('application/rss+xml') + end + end + + describe "category default views" do + it "has a top default view" do + category.update_attributes!(default_view: 'top', default_top_period: 'monthly') + get "/c/#{category.slug}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["for_period"]).to eq("monthly") + end + + it "has a default view of nil" do + category.update_attributes!(default_view: nil) + get "/c/#{category.slug}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["for_period"]).to be_blank + end + + it "has a default view of ''" do + category.update_attributes!(default_view: '') + get "/c/#{category.slug}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["for_period"]).to be_blank + end + + it "has a default view of latest" do + category.update_attributes!(default_view: 'latest') + get "/c/#{category.slug}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["for_period"]).to be_blank + end + end + + describe "renders canonical tag" do + it 'for category default view' do + get "/c/#{category.slug}" + expect(response.status).to eq(200) + expect(css_select("link[rel=canonical]").length).to eq(1) + end + + it 'for category latest view' do + get "/c/#{category.slug}/l/latest" + expect(response.status).to eq(200) + expect(css_select("link[rel=canonical]").length).to eq(1) + end + end + end + end + + describe "topics_by" do + before do + sign_in(Fabricate(:user)) + Fabricate(:topic, user: user) + end + + it "should respond with a list" do + get "/topics/created-by/#{user.username}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["topics"].size).to eq(1) + end + end + + describe "private_messages" do + it "returns 403 error when the user can't see private message" do + sign_in(Fabricate(:user)) + get "/topics/private-messages/#{user.username}.json" + expect(response).to be_forbidden + end + + it "succeeds when the user can see private messages" do + pm = Fabricate(:private_message_topic, user: Fabricate(:user)) + pm.topic_allowed_users.create!(user: user) + sign_in(user) + get "/topics/private-messages/#{user.username}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["topics"].size).to eq(1) + end + end + + describe "private_messages_sent" do + before do + pm = Fabricate(:private_message_topic, user: user) + Fabricate(:post, user: user, topic: pm, post_number: 1) + end + + it "returns 403 error when the user can't see private message" do + sign_in(Fabricate(:user)) + get "/topics/private-messages-sent/#{user.username}.json" + expect(response).to be_forbidden + end + + it "succeeds when the user can see private messages" do + sign_in(user) + get "/topics/private-messages-sent/#{user.username}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["topics"].size).to eq(1) + end + end + + describe "private_messages_unread" do + before do + u = Fabricate(:user) + pm = Fabricate(:private_message_topic, user: u) + Fabricate(:post, user: u, topic: pm, post_number: 1) + pm.topic_allowed_users.create!(user: user) + end + + it "returns 403 error when the user can't see private message" do + sign_in(Fabricate(:user)) + get "/topics/private-messages-unread/#{user.username}.json" + expect(response).to be_forbidden + end + + it "succeeds when the user can see private messages" do + sign_in(user) + get "/topics/private-messages-unread/#{user.username}.json" + expect(response.status).to eq(200) + json = JSON.parse(response.body) + expect(json["topic_list"]["topics"].size).to eq(1) + end + end + + describe 'read' do + it 'raises an error when not logged in' do + get "/read" + expect(response.status).to eq(404) + end + + context 'when logged in' do + it "succeeds" do + sign_in(user) + get "/read" + expect(response.status).to eq(200) + end + end + end + + describe "best_periods_for" do + it "returns yearly for more than 180 days" do + expect(ListController.best_periods_for(nil, :all)).to eq([:yearly]) + expect(ListController.best_periods_for(180.days.ago, :all)).to eq([:yearly]) + end + + it "includes monthly when less than 180 days and more than 35 days" do + (35...180).each do |date| + expect(ListController.best_periods_for(date.days.ago, :all)).to eq([:monthly, :yearly]) + end + end + + it "includes weekly when less than 35 days and more than 8 days" do + (8...35).each do |date| + expect(ListController.best_periods_for(date.days.ago, :all)).to eq([:weekly, :monthly, :yearly]) + end + end + + it "includes daily when less than 8 days" do + (0...8).each do |date| + expect(ListController.best_periods_for(date.days.ago, :all)).to eq([:daily, :weekly, :monthly, :yearly]) + end + end + + it "returns default even for more than 180 days" do + expect(ListController.best_periods_for(nil, :monthly)).to eq([:monthly, :yearly]) + expect(ListController.best_periods_for(180.days.ago, :monthly)).to eq([:monthly, :yearly]) + end + + it "returns default even when less than 180 days and more than 35 days" do + (35...180).each do |date| + expect(ListController.best_periods_for(date.days.ago, :weekly)).to eq([:weekly, :monthly, :yearly]) + end + end + + it "returns default even when less than 35 days and more than 8 days" do + (8...35).each do |date| + expect(ListController.best_periods_for(date.days.ago, :daily)).to eq([:daily, :weekly, :monthly, :yearly]) + end + end + + it "doesn't return default when set to all" do + expect(ListController.best_periods_for(nil, :all)).to eq([:yearly]) + end + + it "doesn't return value twice when matches default" do + expect(ListController.best_periods_for(nil, :yearly)).to eq([:yearly]) + end + end + + describe "categories suppression" do + let(:category_one) { Fabricate(:category) } + let(:sub_category) { Fabricate(:category, parent_category: category_one, suppress_from_latest: true) } + let!(:topic_in_sub_category) { Fabricate(:topic, category: sub_category) } + + let(:category_two) { Fabricate(:category, suppress_from_latest: true) } + let!(:topic_in_category_two) { Fabricate(:topic, category: category_two) } + + it "suppresses categories from the latest list" do + get "/#{SiteSetting.homepage}.json" + expect(response.status).to eq(200) + + topic_titles = JSON.parse(response.body)["topic_list"]["topics"].map { |t| t["title"] } + expect(topic_titles).not_to include(topic_in_sub_category.title, topic_in_category_two.title) + end + + it "does not suppress" do + get "/#{SiteSetting.homepage}.json", params: { category: category_one.id } + expect(response.status).to eq(200) + + topic_titles = JSON.parse(response.body)["topic_list"]["topics"].map { |t| t["title"] } + expect(topic_titles).to include(topic_in_sub_category.title) + end + end + + describe "safe mode" do + it "handles safe mode" do + get "/latest" + expect(response.body).to match(/plugin\.js/) + expect(response.body).to match(/plugin-third-party\.js/) + + get "/latest", params: { safe_mode: "no_plugins" } + expect(response.body).not_to match(/plugin\.js/) + expect(response.body).not_to match(/plugin-third-party\.js/) + + get "/latest", params: { safe_mode: "only_official" } + expect(response.body).to match(/plugin\.js/) + expect(response.body).not_to match(/plugin-third-party\.js/) + end + end end diff --git a/spec/controllers/metadata_controller_spec.rb b/spec/requests/metadata_controller_spec.rb similarity index 76% rename from spec/controllers/metadata_controller_spec.rb rename to spec/requests/metadata_controller_spec.rb index 96558ec0c7..de317972d6 100644 --- a/spec/controllers/metadata_controller_spec.rb +++ b/spec/requests/metadata_controller_spec.rb @@ -1,15 +1,15 @@ require 'rails_helper' RSpec.describe MetadataController do - describe 'manifest.json' do + describe 'manifest.webmanifest' do it 'returns the right output' do - title = 'MyApp' SiteSetting.title = title SiteSetting.large_icon_url = "http://big.square/png" - get :manifest - expect(response.content_type).to eq('application/json') + get "/manifest.webmanifest" + expect(response.status).to eq(200) + expect(response.content_type).to eq('application/manifest+json') manifest = JSON.parse(response.body) expect(manifest["name"]).to eq(title) @@ -18,14 +18,16 @@ RSpec.describe MetadataController do it 'can guess mime types' do SiteSetting.large_icon_url = "http://big.square/ico.jpg" - get :manifest + get "/manifest.webmanifest" + expect(response.status).to eq(200) manifest = JSON.parse(response.body) expect(manifest["icons"].first["type"]).to eq("image/jpeg") end it 'defaults to png' do SiteSetting.large_icon_url = "http://big.square/noidea.bogus" - get :manifest + get "/manifest.webmanifest" + expect(response.status).to eq(200) manifest = JSON.parse(response.body) expect(manifest["icons"].first["type"]).to eq("image/png") end @@ -37,7 +39,8 @@ RSpec.describe MetadataController do favicon_path = '/uploads/something/23432.png' SiteSetting.title = title SiteSetting.favicon_url = favicon_path - get :opensearch, format: :xml + get "/opensearch.xml" + expect(response.status).to eq(200) expect(response.body).to include(title) expect(response.body).to include("/search?q={searchTerms}") expect(response.body).to include('image/png') diff --git a/spec/controllers/notifications_controller_spec.rb b/spec/requests/notifications_controller_spec.rb similarity index 76% rename from spec/controllers/notifications_controller_spec.rb rename to spec/requests/notifications_controller_spec.rb index d322abd848..e22721f82b 100644 --- a/spec/controllers/notifications_controller_spec.rb +++ b/spec/requests/notifications_controller_spec.rb @@ -2,14 +2,19 @@ require 'rails_helper' def create_notification(user_id, resp_code, matcher) notification_count = Notification.count - post :create, params: { notification_type: Notification.types[:mentioned], user_id: user_id, data: { message: 'tada' }.to_json }, format: :json + post "/notifications.json", + params: { + notification_type: Notification.types[:mentioned], + user_id: user_id, + data: { message: 'tada' }.to_json + } expect(response.status).to eq(resp_code) expect(Notification.count).send(matcher, eq(notification_count)) end def update_notification(topic_id, resp_code, matcher) notification = Fabricate(:notification) - post :update, params: { id: notification.id, topic_id: topic_id }, format: :json + put "/notifications/#{notification.id}.json", params: { topic_id: topic_id } expect(response.status).to eq(resp_code) notification.reload expect(notification.topic_id).send(matcher, eq(topic_id)) @@ -18,68 +23,63 @@ end def delete_notification(resp_code, matcher) notification = Fabricate(:notification) notification_count = Notification.count - delete :destroy, params: { id: notification.id }, format: :json + delete "/notifications/#{notification.id}.json" expect(response.status).to eq(resp_code) expect(Notification.count).send(matcher, eq(notification_count)) end describe NotificationsController do - context 'when logged in' do - context 'as normal user' do - - let!(:user) { log_in } + let!(:user) { sign_in(Fabricate(:user)) } describe '#index' do it 'should succeed for recent' do - get :index, params: { recent: true } - expect(response).to be_success + get "/notifications", params: { recent: true } + expect(response.status).to eq(200) end it 'should succeed for history' do - get :index - expect(response).to be_success + get "/notifications" + expect(response.status).to eq(200) end it 'should mark notifications as viewed' do - _notification = Fabricate(:notification, user: user) + Fabricate(:notification, user: user) expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) - get :index, params: { recent: true }, format: :json + get "/notifications.json", params: { recent: true } expect(user.reload.unread_notifications).to eq(0) expect(user.reload.total_unread_notifications).to eq(1) end it 'should not mark notifications as viewed if silent param is present' do - _notification = Fabricate(:notification, user: user) + Fabricate(:notification, user: user) expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) - get :index, params: { recent: true, silent: true } + get "/notifications", params: { recent: true, silent: true } expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) end context 'when username params is not valid' do it 'should raise the right error' do - get :index, params: { username: 'somedude' }, format: :json - - expect(response).to_not be_success + get "/notifications.json", params: { username: 'somedude' } expect(response.status).to eq(404) end end end it 'should succeed' do - put :mark_read, format: :json - expect(response).to be_success + put "/notifications/mark-read.json" + expect(response.status).to eq(200) end it "can update a single notification" do notification = Fabricate(:notification, user: user) notification2 = Fabricate(:notification, user: user) - put :mark_read, params: { id: notification.id }, format: :json - expect(response).to be_success + put "/notifications/mark-read.json", params: { id: notification.id } + expect(response.status).to eq(200) notification.reload notification2.reload @@ -89,10 +89,10 @@ describe NotificationsController do end it "updates the `read` status" do - _notification = Fabricate(:notification, user: user) + Fabricate(:notification, user: user) expect(user.reload.unread_notifications).to eq(1) expect(user.reload.total_unread_notifications).to eq(1) - put :mark_read, format: :json + put "/notifications/mark-read.json" user.reload expect(user.reload.unread_notifications).to eq(0) expect(user.reload.total_unread_notifications).to eq(0) @@ -115,12 +115,10 @@ describe NotificationsController do delete_notification(403, :to) end end - end context 'as admin' do - - let!(:admin) { log_in(:admin) } + let!(:admin) { sign_in(Fabricate(:admin)) } describe '#create' do it "can create notification" do @@ -141,16 +139,14 @@ describe NotificationsController do delete_notification(200, :to_not) end end - end - end context 'when not logged in' do describe '#index' do it 'should raise an error' do - get :index, params: { recent: true }, format: :json + get "/notifications.json", params: { recent: true } expect(response.status).to eq(403) end end @@ -173,7 +169,5 @@ describe NotificationsController do delete_notification(403, :to) end end - end - end diff --git a/spec/controllers/offline_controller_spec.rb b/spec/requests/offline_controller_spec.rb similarity index 84% rename from spec/controllers/offline_controller_spec.rb rename to spec/requests/offline_controller_spec.rb index b583747b7d..3bbea35374 100644 --- a/spec/controllers/offline_controller_spec.rb +++ b/spec/requests/offline_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' describe OfflineController do it "can hit index" do - get :index + get "/offline.html" expect(response.status).to eq(200) end end diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb index 5e0a18edd9..c4deba0b83 100644 --- a/spec/requests/omniauth_callbacks_controller_spec.rb +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -80,7 +80,7 @@ RSpec.describe Users::OmniauthCallbacksController do expect(events.map { |event| event[:event_name] }).to include(:user_logged_in, :user_first_logged_in) - expect(response).to be_success + expect(response.status).to eq(200) response_body = JSON.parse(response.body) @@ -106,7 +106,7 @@ RSpec.describe Users::OmniauthCallbacksController do expect(events.map { |event| event[:event_name] }).to include(:user_logged_in, :user_first_logged_in) - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(user.email_confirmed?).to eq(true) @@ -125,7 +125,7 @@ RSpec.describe Users::OmniauthCallbacksController do expect(events.map { |event| event[:event_name] }).to include(:user_logged_in, :user_first_logged_in) - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(user.staged).to eq(false) @@ -185,7 +185,7 @@ RSpec.describe Users::OmniauthCallbacksController do it 'should return the right response' do get "/auth/google_oauth2/callback.json" - expect(response).to be_success + expect(response.status).to eq(200) response_body = JSON.parse(response.body) diff --git a/spec/requests/onebox_controller_spec.rb b/spec/requests/onebox_controller_spec.rb new file mode 100644 index 0000000000..21728df3e7 --- /dev/null +++ b/spec/requests/onebox_controller_spec.rb @@ -0,0 +1,182 @@ +require 'rails_helper' + +describe OneboxController do + + let(:url) { "http://google.com" } + + it "requires the user to be logged in" do + get "/onebox.json", params: { url: url } + expect(response.status).to eq(403) + end + + describe "logged in" do + let(:user) { Fabricate(:user) } + let(:html) do + html = <<~HTML + + + + + + +

body

+ + + HTML + html + end + + let(:html2) do + html = <<~HTML + + + + + + +

body

+ + + HTML + html + end + + def bypass_limiting + Oneboxer.onebox_previewed!(user.id) + end + + before do + sign_in(user) + end + + it 'invalidates the cache if refresh is passed' do + stub_request(:head, url) + stub_request(:get, url).to_return(status: 200, body: html).then.to_raise + + bypass_limiting + Rails.cache.delete("onebox__#{url}") + get "/onebox.json", params: { url: url } + expect(response.status).to eq(200) + expect(response.body).to include("Onebox1") + + bypass_limiting + stub_request(:get, url).to_return(status: 200, body: html2).then.to_raise + get "/onebox.json", params: { url: url, refresh: 'true' } + expect(response.status).to eq(200) + expect(response.body).to include("Onebox2") + end + + describe "cached onebox" do + it "returns the cached onebox response in the body" do + url = "http://noodle.com/" + + stub_request(:head, url) + stub_request(:get, url).to_return(body: html).then.to_raise + + get "/onebox.json", params: { url: url, refresh: "true" } + + expect(response.status).to eq(200) + expect(response.body).to include('Onebox1') + expect(response.body).to include('bodycontent') + + get "/onebox.json", params: { url: url } + expect(response.status).to eq(200) + expect(response.body).to include('Onebox1') + expect(response.body).to include('bodycontent') + end + end + + describe "only 1 outgoing preview per user" do + it "returns 429" do + Oneboxer.preview_onebox!(user.id) + + stub_request(:head, url) + stub_request(:get, url).to_return(body: html).then.to_raise + + get "/onebox.json", params: { url: url, refresh: "true" } + expect(response.status).to eq(429) + end + end + + describe "found onebox" do + it 'returns the onebox response in the body' do + stub_request(:head, url) + stub_request(:get, url).to_return(body: html).then.to_raise + get "/onebox.json", params: { url: url, refresh: "true" } + + expect(response.status).to eq(200) + expect(response.body).to include("Onebox1") + end + end + + describe "missing onebox" do + it "returns 404 if the onebox is nil" do + stub_request(:head, url) + stub_request(:get, url).to_return(body: nil).then.to_raise + get "/onebox.json", params: { url: url, refresh: "true" } + expect(response.response_code).to eq(404) + end + + it "returns 404 if the onebox is an empty string" do + stub_request(:head, url) + stub_request(:get, url).to_return(body: " \t ").then.to_raise + get "/onebox.json", params: { url: url, refresh: "true" } + expect(response.response_code).to eq(404) + end + end + + describe "local onebox" do + it 'does not cache local oneboxes' do + post = create_post + url = Discourse.base_url + post.url + + get "/onebox.json", params: { url: url, category_id: post.topic.category_id } + expect(response.body).to include('blockquote') + + post.trash! + + get "/onebox.json", params: { url: url, category_id: post.topic.category_id } + expect(response.body).not_to include('blockquote') + end + end + + it 'does not onebox when you have no permission on category' do + post = create_post + url = Discourse.base_url + post.url + + get "/onebox.json", params: { url: url, category_id: post.topic.category_id } + expect(response.body).to include('blockquote') + + post.topic.category.set_permissions(staff: :full) + post.topic.category.save + + get "/onebox.json", params: { url: url, category_id: post.topic.category_id } + expect(response.body).not_to include('blockquote') + end + + it 'does not allow onebox of PMs' do + post = create_post(archetype: 'private_message', target_usernames: [user.username]) + url = Discourse.base_url + post.url + + get "/onebox.json", params: { url: url } + expect(response.body).not_to include('blockquote') + end + + it 'does not allow whisper onebox' do + post = create_post + whisper = create_post(topic_id: post.topic_id, post_type: Post.types[:whisper]) + url = Discourse.base_url + whisper.url + + get "/onebox.json", params: { url: url } + expect(response.body).not_to include('blockquote') + end + + it 'allows onebox to public topics/posts in PM' do + post = create_post + url = Discourse.base_url + post.url + + get "/onebox.json", params: { url: url } + expect(response.body).to include('blockquote') + end + end +end diff --git a/spec/controllers/permalinks_controller_spec.rb b/spec/requests/permalinks_controller_spec.rb similarity index 53% rename from spec/controllers/permalinks_controller_spec.rb rename to spec/requests/permalinks_controller_spec.rb index 595571f9bd..b172315c63 100644 --- a/spec/controllers/permalinks_controller_spec.rb +++ b/spec/requests/permalinks_controller_spec.rb @@ -1,46 +1,49 @@ require 'rails_helper' describe PermalinksController do + let(:topic) { Fabricate(:topic) } + let(:permalink) { Fabricate(:permalink, url: "deadroutee/topic/546") } + describe 'show' do it "should redirect to a permalink's target_url with status 301" do - permalink = Fabricate(:permalink) - Permalink.any_instance.stubs(:target_url).returns('/t/the-topic-slug/42') - get :show, params: { url: permalink.url } - expect(response).to redirect_to('/t/the-topic-slug/42') + permalink.update!(topic_id: topic.id) + + get "/#{permalink.url}" + + expect(response).to redirect_to(topic.relative_url) expect(response.status).to eq(301) end it "should work for subfolder installs too" do + permalink.update!(topic_id: topic.id) GlobalSetting.stubs(:relative_url_root).returns('/forum') Discourse.stubs(:base_uri).returns("/forum") - permalink = Fabricate(:permalink) - Permalink.any_instance.stubs(:target_url).returns('/forum/t/the-topic-slug/42') - get :show, params: { url: permalink.url } - expect(response).to redirect_to('/forum/t/the-topic-slug/42') + + get "/#{permalink.url}" + + expect(response).to redirect_to(topic.relative_url) expect(response.status).to eq(301) end it "should apply normalizations" do + permalink.update!(external_url: '/topic/100') SiteSetting.permalink_normalizations = "/(.*)\\?.*/\\1" - permalink = Fabricate(:permalink, url: '/topic/bla', external_url: '/topic/100') - - get :show, params: { url: permalink.url, test: "hello" } + get "/#{permalink.url}", params: { test: "hello" } expect(response).to redirect_to('/topic/100') expect(response.status).to eq(301) SiteSetting.permalink_normalizations = "/(.*)\\?.*/\\1X" - get :show, params: { url: permalink.url, test: "hello" } + get "/#{permalink.url}", params: { test: "hello" } expect(response.status).to eq(404) end it 'return 404 if permalink record does not exist' do - get :show, params: { url: '/not/a/valid/url' } + get '/not/a/valid/url' expect(response.status).to eq(404) end end - end diff --git a/spec/controllers/post_action_users_controller_spec.rb b/spec/requests/post_action_users_controller_spec.rb similarity index 67% rename from spec/controllers/post_action_users_controller_spec.rb rename to spec/requests/post_action_users_controller_spec.rb index fdd6add07c..1fdf4bb529 100644 --- a/spec/controllers/post_action_users_controller_spec.rb +++ b/spec/requests/post_action_users_controller_spec.rb @@ -1,17 +1,16 @@ require 'rails_helper' describe PostActionUsersController do - let(:post) { Fabricate(:post, user: log_in) } + let(:post) { Fabricate(:post, user: sign_in(Fabricate(:user))) } context 'with render' do - render_views it 'always allows you to see your own actions' do notify_mod = PostActionType.types[:notify_moderators] PostAction.act(post.user, post, notify_mod, message: 'well something is wrong here!') PostAction.act(Fabricate(:user), post, notify_mod, message: 'well something is not wrong here!') - get :index, params: { id: post.id, post_action_type_id: notify_mod }, format: :json + get "/post_action_users.json", params: { id: post.id, post_action_type_id: notify_mod } expect(response.status).to eq(200) json = JSON.parse(response.body) users = json["post_action_users"] @@ -22,44 +21,39 @@ describe PostActionUsersController do end it 'raises an error without an id' do - expect do - get :index, - params: { post_action_type_id: PostActionType.types[:like] }, - format: :json - end.to raise_error(ActionController::ParameterMissing) + get "/post_action_users.json", params: { post_action_type_id: PostActionType.types[:like] } + expect(response.status).to eq(400) end it 'raises an error without a post action type' do - expect do - get :index, params: { id: post.id }, format: :json - end.to raise_error(ActionController::ParameterMissing) + get "/post_action_users.json", params: { id: post.id } + expect(response.status).to eq(400) end it "fails when the user doesn't have permission to see the post" do - Guardian.any_instance.expects(:can_see?).with(post).returns(false) - - get :index, params: { + post.trash! + get "/post_action_users.json", params: { id: post.id, post_action_type_id: PostActionType.types[:like] - }, format: :json + } expect(response).to be_forbidden end it 'raises an error when anon tries to look at an invalid action' do - get :index, params: { + get "/post_action_users.json", params: { id: Fabricate(:post).id, post_action_type_id: PostActionType.types[:notify_moderators] - }, format: :json + } expect(response).to be_forbidden end it 'succeeds' do - get :index, params: { + get "/post_action_users.json", params: { id: post.id, post_action_type_id: PostActionType.types[:like] } - expect(response).to be_success + expect(response.status).to eq(200) end it "paginates post actions" do @@ -70,7 +64,8 @@ describe PostActionUsersController do PostAction.act(user, post, PostActionType.types[:like]) end - get :index, params: { id: post.id, post_action_type_id: PostActionType.types[:like], page: 1, limit: 2 }, format: :json + get "/post_action_users.json", + params: { id: post.id, post_action_type_id: PostActionType.types[:like], page: 1, limit: 2 } json = JSON.parse(response.body) users = json["post_action_users"] diff --git a/spec/requests/post_actions_controller_spec.rb b/spec/requests/post_actions_controller_spec.rb index cb20f94f3a..882b107eb7 100644 --- a/spec/requests/post_actions_controller_spec.rb +++ b/spec/requests/post_actions_controller_spec.rb @@ -5,18 +5,112 @@ RSpec.describe PostActionsController do let(:post) { Fabricate(:post, user: Fabricate(:coding_horror)) } it 'requires you to be logged in' do - delete '/post_action.json', params: { id: post.id } - expect(response.status).to eq(404) + delete "/post_actions/#{post.id}.json" + expect(response.status).to eq(403) + end + + context 'logged in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) + end + + it 'raises an error when the post_action_type_id is missing' do + delete "/post_actions/#{post.id}.json" + expect(response.status).to eq(400) + end + + it "returns 404 when the post action type doesn't exist for that user" do + delete "/post_actions/#{post.id}.json", params: { post_action_type_id: PostActionType.types[:bookmark] } + expect(response.status).to eq(404) + end + + context 'with a post_action record ' do + let!(:post_action) do + PostAction.create!( + user_id: user.id, + post_id: post.id, + post_action_type_id: PostActionType.types[:bookmark] + ) + end + + it 'returns success' do + delete "/post_actions/#{post.id}.json", params: { post_action_type_id: PostActionType.types[:bookmark] } + expect(response.status).to eq(200) + end + + it 'deletes the action' do + delete "/post_actions/#{post.id}.json", params: { + post_action_type_id: PostActionType.types[:bookmark] + } + + expect(response.status).to eq(200) + expect(PostAction.exists?( + user_id: user.id, + post_id: post.id, + post_action_type_id: PostActionType.types[:bookmark], + deleted_at: nil + )).to eq(false) + end + + it "isn't deleted when the user doesn't have permission" do + pa = PostAction.create!( + post: post, + user: user, + post_action_type_id: PostActionType.types[:like], + created_at: 1.day.ago + ) + + delete "/post_actions/#{post.id}.json", params: { + post_action_type_id: PostActionType.types[:like] + } + + expect(response).to be_forbidden + end + end end end describe '#create' do - it 'requires you to be logged in' do post '/post_actions.json' expect(response.status).to eq(403) end + it 'fails when the user does not have permission to see the post' do + sign_in(Fabricate(:user)) + pm = Fabricate(:private_message_post, user: Fabricate(:coding_horror)) + + post "/post_actions.json", params: { + id: pm.id, + post_action_type_id: PostActionType.types[:bookmark] + } + + expect(response.status).to eq(403) + end + + it 'fails when the user tries to notify user that has disabled PM' do + sign_in(Fabricate(:user)) + user2 = Fabricate(:user) + + post = Fabricate(:post, user: user2) + user2.user_option.update!(allow_private_messages: false) + + post "/post_actions.json", params: { + id: post.id, + post_action_type_id: PostActionType.types[:notify_user], + message: 'testing', + flag_topic: false + } + + expect(response.status).to eq(422) + + expect(JSON.parse(response.body)["errors"].first).to eq(I18n.t( + :not_accepting_pms, username: user2.username + )) + end + describe 'as a moderator' do let(:user) { Fabricate(:moderator) } let(:post_1) { Fabricate(:post, user: Fabricate(:coding_horror)) } @@ -64,12 +158,13 @@ RSpec.describe PostActionsController do post_action = PostAction.last + expect(response.status).to eq(200) expect(post_action.post_id).to eq(post_1.id) expect(post_action.post_action_type_id).to eq(PostActionType.types[:like]) end it "passes a list of taken actions through" do - PostAction.create( + PostAction.create!( post_id: post_1.id, user_id: user.id, post_action_type_id: PostActionType.types[:inappropriate] @@ -79,7 +174,7 @@ RSpec.describe PostActionsController do id: post_1.id, post_action_type_id: PostActionType.types[:off_topic] } - expect(response).to_not be_success + expect(response).to be_forbidden end it 'passes the message through' do @@ -91,6 +186,7 @@ RSpec.describe PostActionsController do message: message } + expect(response.status).to eq(200) expect(PostAction.last.post_id).to eq(post_1.id) expect(Post.last.raw).to include(message) end @@ -105,6 +201,7 @@ RSpec.describe PostActionsController do is_warning: true } + expect(response.status).to eq(200) expect(PostAction.last.post_id).to eq(post_1.id) post = Post.last @@ -133,7 +230,7 @@ RSpec.describe PostActionsController do take_action: 'true' } - expect(response).to be_success + expect(response.status).to eq(200) post_action = PostAction.last @@ -149,7 +246,7 @@ RSpec.describe PostActionsController do post_action_type_id: PostActionType.types[:like] } - expect(response).to be_success + expect(response.status).to eq(200) post_action = PostAction.last @@ -158,4 +255,71 @@ RSpec.describe PostActionsController do end end end + + describe '#defer_flags' do + let!(:flag) do + PostAction.create!( + post_id: flagged_post.id, + user_id: Fabricate(:user).id, + post_action_type_id: PostActionType.types[:spam] + ) + end + let(:flagged_post) { Fabricate(:post, user: Fabricate(:coding_horror)) } + + context "not logged in" do + it "should not allow them to clear flags" do + post "/post_actions/defer_flags.json", params: { id: flagged_post.id } + expect(response.status).to eq(403) + flag.reload + expect(flag.deferred_at).to be_nil + end + end + + context 'logged in' do + let!(:user) { sign_in(Fabricate(:moderator)) } + + it "raises an error without a post_action_type_id" do + post "/post_actions/defer_flags.json", params: { id: flagged_post.id } + expect(response.status).to eq(400) + flag.reload + expect(flag.deferred_at).to be_nil + end + + it "raises an error when the user doesn't have access" do + sign_in(Fabricate(:user)) + + post "/post_actions/defer_flags.json", params: { + id: flagged_post.id, post_action_type_id: PostActionType.types[:spam] + } + + expect(response).to be_forbidden + flag.reload + expect(flag.deferred_at).to be_nil + end + + context "success" do + it "delegates to defer_flags" do + post "/post_actions/defer_flags.json", params: { + id: flagged_post.id, post_action_type_id: PostActionType.types[:spam] + } + + expect(response.status).to eq(200) + flag.reload + expect(flag.deferred_at).to be_present + end + + it "works with a deleted post" do + flagged_post.trash!(user) + + post "/post_actions/defer_flags.json", params: { + id: flagged_post.id, post_action_type_id: PostActionType.types[:spam] + } + + expect(response.status).to eq(200) + flag.reload + expect(flag.deferred_at).to be_present + end + end + end + end end diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb index f43a5b511f..7f6620bd05 100644 --- a/spec/requests/posts_controller_spec.rb +++ b/spec/requests/posts_controller_spec.rb @@ -14,7 +14,7 @@ shared_examples 'finding and showing post' do it 'succeeds' do get url - expect(response).to be_success + expect(response.status).to eq(200) end context "deleted post" do @@ -36,13 +36,13 @@ shared_examples 'finding and showing post' do it "can find posts as a moderator" do sign_in(Fabricate(:moderator)) get url - expect(response).to be_success + expect(response.status).to eq(200) end it "can find posts as a admin" do sign_in(Fabricate(:admin)) get url - expect(response).to be_success + expect(response.status).to eq(200) end end end @@ -62,7 +62,7 @@ describe PostsController do let(:topicless_post) { Fabricate(:post, user: user, raw: '

Car 54, where are you?

') } let(:private_topic) do - Fabricate(:topic, archetype: Archetype.private_message, category: nil) + Fabricate(:topic, archetype: Archetype.private_message, category_id: nil) end let(:private_post) { Fabricate(:post, user: user, topic: private_topic) } @@ -127,7 +127,7 @@ describe PostsController do it 'does not allow to destroy when edit time limit expired' do SiteSetting.post_edit_time_limit = 5 - post = Fabricate(:post, topic_id: topic.id, created_at: 10.minutes.ago, user_id: user.id, post_number: 3) + post = Fabricate(:post, topic: topic, created_at: 10.minutes.ago, user: user, post_number: 3) sign_in(user) delete "/posts/#{post.id}.json" @@ -165,7 +165,7 @@ describe PostsController do describe 'when logged in' do let(:poster) { Fabricate(:moderator) } let(:post1) { Fabricate(:post, user: poster, post_number: 2) } - let(:post2) { Fabricate(:post, topic_id: post1.topic_id, user: poster, post_number: 3, reply_to_post_number: post1.post_number) } + let(:post2) { Fabricate(:post, topic: post1.topic, user: poster, post_number: 3, reply_to_post_number: post1.post_number) } it "raises invalid parameters no post_ids" do sign_in(poster) @@ -277,7 +277,7 @@ describe PostsController do it 'passes the edit reason through' do put "/posts/#{post.id}.json", params: update_params - expect(response).to be_success + expect(response.status).to eq(200) post.reload expect(post.edit_reason).to eq("typo") expect(post.raw).to eq("edited body") @@ -307,7 +307,7 @@ describe PostsController do put "/posts/#{post.id}.json", params: param - expect(response).to be_success + expect(response.status).to eq(200) expect(TopicLink.count).to eq(1) end @@ -316,7 +316,7 @@ describe PostsController do PostDestroyer.new(moderator, first_post).destroy put "/posts/#{first_post.id}.json", params: update_params - expect(response).not_to be_success + expect(response).not_to be_successful end end @@ -330,7 +330,7 @@ describe PostsController do PostDestroyer.new(moderator, first_post).destroy put "/posts/#{first_post.id}.json", params: update_params - expect(response).to be_success + expect(response.status).to eq(200) post.reload expect(post.raw).to eq('edited body') @@ -356,14 +356,14 @@ describe PostsController do describe '#bookmark' do include_examples 'action requires login', :put, "/posts/2/bookmark.json" + let(:post) { Fabricate(:post, user: user) } + let(:user) { Fabricate(:user) } describe 'when logged in' do before do sign_in(user) end - let(:user) { Fabricate(:user) } - let(:post) { Fabricate(:post, user: user) } let(:private_message) { Fabricate(:private_message_post) } it "raises an error if the user doesn't have permission to see the post" do @@ -373,7 +373,7 @@ describe PostsController do it 'creates a bookmark' do put "/posts/#{post.id}/bookmark.json", params: { bookmarked: "true" } - expect(response).to be_success + expect(response.status).to eq(200) post_action = PostAction.find_by(user: user, post: post) expect(post_action.post_action_type_id).to eq(PostActionType.types[:bookmark]) @@ -423,6 +423,71 @@ describe PostsController do end end end + + context "api" do + let(:api_key) { user.generate_api_key(user) } + let(:master_key) { ApiKey.create_master_key } + + # choosing an arbitrarily easy to mock trusted activity + it 'allows users with api key to bookmark posts' do + put "/posts/#{post.id}/bookmark.json", params: { + bookmarked: "true", + api_key: api_key.key + } + + expect(response.status).to eq(200) + expect(PostAction.where( + post: post, + user: user, + post_action_type_id: PostActionType.types[:bookmark] + ).count).to eq(1) + end + + it 'raises an error with a user key that does not match an optionally specified username' do + put "/posts/#{post.id}/bookmark.json", params: { + bookmarked: "true", + api_key: api_key.key, + api_username: 'made_up' + } + + expect(response.status).to eq(403) + end + + it 'allows users with a master api key to bookmark posts' do + put "/posts/#{post.id}/bookmark.json", params: { + bookmarked: "true", + api_key: master_key.key, + api_username: user.username + } + + expect(response.status).to eq(200) + expect(PostAction.where( + post: post, + user: user, + post_action_type_id: PostActionType.types[:bookmark] + ).count).to eq(1) + end + + it 'disallows phonies to bookmark posts' do + put "/posts/#{post.id}/bookmark.json", params: { + bookmarked: "true", + api_key: SecureRandom.hex(32), + api_username: user.username + } + + expect(response.status).to eq(403) + end + + it 'disallows blank api' do + put "/posts/#{post.id}/bookmark.json", params: { + bookmarked: "true", + api_key: "", + api_username: user.username + } + + expect(response.status).to eq(403) + end + end end describe '#wiki' do @@ -522,7 +587,7 @@ describe PostsController do it "can rebake the post" do sign_in(Fabricate(:moderator)) put "/posts/#{post.id}/rebake.json" - expect(response).to be_success + expect(response.status).to eq(200) end end end @@ -552,7 +617,7 @@ describe PostsController do wpid: 1 } - expect(response).to be_success + expect(response.status).to eq(200) original = response.body post "/posts.json", params: { @@ -563,7 +628,7 @@ describe PostsController do wpid: 2 } - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to eq(original) end @@ -582,7 +647,7 @@ describe PostsController do reply_to_post_number: 1 } - expect(response).to be_success + expect(response.status).to eq(200) expect(post_1.topic.user.notifications.count).to eq(1) post_1.topic.user.notifications.destroy_all @@ -595,7 +660,7 @@ describe PostsController do import_mode: true } - expect(response).to be_success + expect(response.status).to eq(200) expect(post_1.topic.user.notifications.count).to eq(0) post "/posts.json", params: { @@ -607,7 +672,7 @@ describe PostsController do import_mode: false } - expect(response).to be_success + expect(response.status).to eq(200) expect(post_1.topic.user.notifications.count).to eq(1) end end @@ -629,7 +694,7 @@ describe PostsController do title: 'this is the test title for the topic' } - expect(response).to be_success + expect(response.status).to eq(200) parsed = ::JSON.parse(response.body) expect(parsed["action"]).to eq("enqueued") @@ -655,7 +720,7 @@ describe PostsController do topic_id: topic.id } - expect(response).not_to be_success + expect(response).not_to be_successful parsed = ::JSON.parse(response.body) expect(parsed["action"]).not_to eq("enqueued") end @@ -668,7 +733,7 @@ describe PostsController do title: 'this is the test title for the topic' } - expect(response).not_to be_success + expect(response).not_to be_successful parsed = ::JSON.parse(response.body) expect(parsed["action"]).not_to eq("enqueued") end @@ -682,7 +747,7 @@ describe PostsController do title: 'when I eat s3 sometimes when not looking' } - expect(response).to be_success + expect(response.status).to eq(200) parsed = ::JSON.parse(response.body) expect(parsed["action"]).to eq("enqueued") @@ -703,7 +768,7 @@ describe PostsController do archetype: Archetype.private_message } - expect(response).not_to be_success + expect(response).not_to be_successful # allow pm to this group group.update_columns(messageable_level: Group::ALIAS_LEVELS[:everyone]) @@ -715,7 +780,7 @@ describe PostsController do archetype: Archetype.private_message } - expect(response).to be_success + expect(response.status).to eq(200) parsed = ::JSON.parse(response.body) post = Post.find(parsed['id']) @@ -731,7 +796,7 @@ describe PostsController do nested_post: true } - expect(response).to be_success + expect(response.status).to eq(200) parsed = ::JSON.parse(response.body) expect(parsed['post']).to be_present expect(parsed['post']['cooked']).to be_present @@ -742,10 +807,10 @@ describe PostsController do title = "this is a title #{SecureRandom.hash}" post "/posts.json", params: { raw: raw, title: title, wpid: 1 } - expect(response).to be_success + expect(response.status).to eq(200) post "/posts.json", params: { raw: raw, title: title, wpid: 2 } - expect(response).not_to be_success + expect(response).not_to be_successful end it 'can not create a post in a disallowed category' do @@ -770,7 +835,7 @@ describe PostsController do meta_data: { xyz: 'abc' } } - expect(response).to be_success + expect(response.status).to eq(200) new_post = Post.last topic = new_post.topic @@ -793,7 +858,7 @@ describe PostsController do image_sizes: { width: '100', height: '200' } } - expect(response).to be_success + expect(response.status).to eq(200) new_post = Post.last topic = new_post.topic @@ -818,7 +883,7 @@ describe PostsController do target_usernames: "#{user_2.username},#{user_3.username}" } - expect(response).to be_success + expect(response.status).to eq(200) new_post = Post.last new_topic = Topic.last @@ -831,7 +896,7 @@ describe PostsController do context "errors" do it "does not succeed" do post "/posts.json", params: { raw: 'test' } - expect(response).not_to be_success + expect(response).not_to be_successful expect(response.status).to eq(422) end @@ -860,7 +925,7 @@ describe PostsController do category: destination_category.id, shared_draft: 'true' } - expect(response).not_to be_success + expect(response).not_to be_successful end describe "as a staff user" do @@ -875,7 +940,7 @@ describe PostsController do category: destination_category.id, shared_draft: 'true' } - expect(response).not_to be_success + expect(response).not_to be_successful end context "with a shared category" do @@ -891,7 +956,7 @@ describe PostsController do category: destination_category.id, shared_draft: 'true' } - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body) topic = Topic.find(result['topic_id']) expect(topic.category_id).to eq(shared_category.id) @@ -918,7 +983,7 @@ describe PostsController do is_warning: true } - expect(response).to be_success + expect(response.status).to eq(200) new_topic = Topic.last @@ -935,7 +1000,7 @@ describe PostsController do is_warning: false } - expect(response).to be_success + expect(response.status).to eq(200) new_topic = Topic.last @@ -955,7 +1020,7 @@ describe PostsController do is_warning: true } - expect(response).to be_success + expect(response.status).to eq(200) new_topic = Topic.last @@ -993,7 +1058,7 @@ describe PostsController do it "ensures staff can see the revisions" do sign_in(Fabricate(:admin)) get "/posts/#{post.id}/revisions/#{post_revision.number}.json" - expect(response).to be_success + expect(response.status).to eq(200) end it "ensures poster can see the revisions" do @@ -1004,13 +1069,13 @@ describe PostsController do pr = Fabricate(:post_revision, user: user, post: post) get "/posts/#{pr.post_id}/revisions/#{pr.number}.json" - expect(response).to be_success + expect(response.status).to eq(200) end it "ensures trust level 4 can see the revisions" do sign_in(Fabricate(:user, trust_level: 4)) get "/posts/#{post_revision.post_id}/revisions/#{post_revision.number}.json" - expect(response).to be_success + expect(response.status).to eq(200) end end @@ -1020,7 +1085,7 @@ describe PostsController do it "ensures anyone can see the revisions" do get "/posts/#{post_revision.post_id}/revisions/#{post_revision.number}.json" - expect(response).to be_success + expect(response.status).to eq(200) end end @@ -1034,7 +1099,7 @@ describe PostsController do it "also work on deleted post" do sign_in(admin) get "/posts/#{deleted_post_revision.post_id}/revisions/#{deleted_post_revision.number}.json" - expect(response).to be_success + expect(response.status).to eq(200) end end @@ -1049,7 +1114,7 @@ describe PostsController do it "also work on deleted topic" do sign_in(admin) get "/posts/#{post_revision.post_id}/revisions/#{post_revision.number}.json" - expect(response).to be_success + expect(response.status).to eq(200) end end end @@ -1070,7 +1135,7 @@ describe PostsController do it "does not work" do sign_in(Fabricate(:user)) put "/posts/#{post_id}/revisions/#{revision_id}/revert.json" - expect(response).to_not be_success + expect(response).to_not be_successful end end @@ -1086,12 +1151,12 @@ describe PostsController do it "fails when post_revision record is not found" do put "/posts/#{post_id}/revisions/#{revision_id + 1}/revert.json" - expect(response).to_not be_success + expect(response).to_not be_successful end it "fails when post record is not found" do put "/posts/#{post_id + 1}/revisions/#{revision_id}/revert.json" - expect(response).to_not be_success + expect(response).to_not be_successful end it "fails when revision is blank" do @@ -1108,7 +1173,7 @@ describe PostsController do it "works!" do put "/posts/#{post_id}/revisions/#{revision_id}/revert.json" - expect(response).to be_success + expect(response.status).to eq(200) end it "supports reverting posts in deleted topics" do @@ -1116,7 +1181,7 @@ describe PostsController do PostDestroyer.new(moderator, first_post).destroy put "/posts/#{post_id}/revisions/#{revision_id}/revert.json" - expect(response).to be_success + expect(response.status).to eq(200) end end end @@ -1131,13 +1196,13 @@ describe PostsController do it "raises an error when you can't see the post" do post = Fabricate(:private_message_post) get "/posts/#{post.id}/expand-embed.json" - expect(response).not_to be_success + expect(response).not_to be_successful end it "retrieves the body when you can see the post" do TopicEmbed.expects(:expanded_for).with(post).returns("full content") get "/posts/#{post.id}/expand-embed.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['cooked']).to eq("full content") end end @@ -1155,7 +1220,7 @@ describe PostsController do it "can see the flagged posts when authorized" do sign_in(Fabricate(:moderator)) get "/posts/system/flagged.json" - expect(response).to be_success + expect(response.status).to eq(200) end it "only shows agreed and deferred flags" do @@ -1176,7 +1241,7 @@ describe PostsController do sign_in(Fabricate(:moderator)) get "/posts/#{user.username}/flagged.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body).length).to eq(2) end @@ -1196,7 +1261,7 @@ describe PostsController do it "can see the deleted posts when authorized" do sign_in(Fabricate(:moderator)) get "/posts/system/deleted.json" - expect(response).to be_success + expect(response.status).to eq(200) end it "doesn't return secured categories for moderators if they don't have access" do @@ -1213,7 +1278,7 @@ describe PostsController do sign_in(Fabricate(:moderator)) get "/posts/#{user.username}/deleted.json" - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data.length).to eq(0) @@ -1229,7 +1294,7 @@ describe PostsController do sign_in(Fabricate(:moderator)) get "/posts/#{user.username}/deleted.json" - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data.length).to eq(0) @@ -1248,7 +1313,7 @@ describe PostsController do sign_in(Fabricate(:admin)) get "/posts/#{user.username}/deleted.json" - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data.length).to eq(1) @@ -1262,7 +1327,7 @@ describe PostsController do it "can be viewed by anonymous" do post = Fabricate(:post, raw: "123456789") get "/posts/#{post.id}/raw.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to eq("123456789") end end @@ -1273,7 +1338,7 @@ describe PostsController do post = Fabricate(:post, topic: topic, post_number: 1, raw: "123456789") post.save get "/raw/#{topic.id}/1.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to eq("123456789") end end @@ -1301,7 +1366,7 @@ describe PostsController do get "/u/#{user.username}/activity.rss" - expect(response).to be_success + expect(response.status).to eq(200) body = response.body @@ -1319,7 +1384,7 @@ describe PostsController do private_post get "/private-posts.rss" - expect(response).to be_success + expect(response.status).to eq(200) body = response.body @@ -1333,7 +1398,7 @@ describe PostsController do public_post private_post get "/private-posts.json" - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) post_ids = json['private_posts'].map { |p| p['id'] } @@ -1350,7 +1415,7 @@ describe PostsController do get "/posts.rss" - expect(response).to be_success + expect(response.status).to eq(200) body = response.body @@ -1366,7 +1431,7 @@ describe PostsController do topicless_post get "/posts.json" - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) post_ids = json['latest_posts'].map { |p| p['id'] } @@ -1383,7 +1448,7 @@ describe PostsController do post = Fabricate(:post, cooked: "WAt") get "/posts/#{post.id}/cooked.json" - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json).to be_present @@ -1408,7 +1473,7 @@ describe PostsController do sign_in(Fabricate(:moderator)) get "/posts/#{post.id}/raw-email.json" - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json['raw_email']).to eq('email_content') @@ -1423,12 +1488,12 @@ describe PostsController do it 'can lock and unlock the post' do put "/posts/#{public_post.id}/locked.json", params: { locked: "true" } - expect(response).to be_success + expect(response.status).to eq(200) public_post.reload expect(public_post).to be_locked put "/posts/#{public_post.id}/locked.json", params: { locked: "false" } - expect(response).to be_success + expect(response.status).to eq(200) public_post.reload expect(public_post).not_to be_locked end diff --git a/spec/requests/queued_posts_controller_spec.rb b/spec/requests/queued_posts_controller_spec.rb new file mode 100644 index 0000000000..d8911e52e1 --- /dev/null +++ b/spec/requests/queued_posts_controller_spec.rb @@ -0,0 +1,125 @@ +require 'rails_helper' +require_dependency 'queued_posts_controller' +require_dependency 'queued_post' + +describe QueuedPostsController do + context 'without authentication' do + it 'fails' do + get "/queued-posts.json" + expect(response).to be_forbidden + end + end + + context 'as a regular user' do + before { sign_in(Fabricate(:user)) } + + it 'fails' do + get "/queued-posts.json" + expect(response).to be_forbidden + end + end + + context 'as an admin' do + before { sign_in(Fabricate(:moderator)) } + + it 'returns the queued posts' do + get "/queued-posts.json" + expect(response.status).to eq(200) + end + end + + describe '#update' do + before { sign_in(Fabricate(:moderator)) } + let(:qp) { Fabricate(:queued_post) } + + context 'not found' do + it 'returns json error' do + qp.destroy! + + put "/queued_posts/#{qp.id}.json", params: { + queued_post: { state: 'approved' } + } + + expect(response.status).to eq(422) + + expect(JSON.parse(response.body)["errors"].first).to eq(I18n.t('queue.not_found')) + end + end + + context 'approved' do + it 'updates the post to approved' do + + put "/queued_posts/#{qp.id}.json", params: { + queued_post: { state: 'approved' } + } + + expect(response.status).to eq(200) + + qp.reload + expect(qp.state).to eq(QueuedPost.states[:approved]) + end + end + + context 'rejected' do + it 'updates the post to rejected' do + + put "/queued_posts/#{qp.id}.json", params: { + queued_post: { state: 'rejected' } + } + + expect(response.status).to eq(200) + + qp.reload + expect(qp.state).to eq(QueuedPost.states[:rejected]) + end + end + + context 'editing content' do + let(:changes) do + { + raw: 'new raw', + title: 'new title', + category_id: 10, + tags: ['new_tag'] + } + end + + context 'when it is a topic' do + let(:queued_topic) { Fabricate(:queued_topic) } + + it 'updates the topic attributes' do + put "/queued_posts/#{queued_topic.id}.json", params: { + queued_post: changes + } + + expect(response.status).to eq(200) + queued_topic.reload + + expect(queued_topic.raw).to eq(changes[:raw]) + expect(queued_topic.post_options['title']).to eq(changes[:title]) + expect(queued_topic.post_options['category']).to eq(changes[:category_id]) + expect(queued_topic.post_options['tags']).to eq(changes[:tags]) + end + end + + context 'when it is a reply' do + let(:queued_reply) { Fabricate(:queued_post) } + + it 'updates the reply attributes' do + put "/queued_posts/#{queued_reply.id}.json", params: { + queued_post: changes + } + + original_category = queued_reply.post_options['category'] + expect(response.status).to eq(200) + queued_reply.reload + + expect(queued_reply.raw).to eq(changes[:raw]) + expect(queued_reply.post_options['title']).to be_nil + expect(queued_reply.post_options['category']).to eq(original_category) + expect(queued_reply.post_options['tags']).to be_nil + end + end + end + end +end diff --git a/spec/controllers/search_controller_spec.rb b/spec/requests/search_controller_spec.rb similarity index 70% rename from spec/controllers/search_controller_spec.rb rename to spec/requests/search_controller_spec.rb index f81f9ef937..84346d1b8d 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/requests/search_controller_spec.rb @@ -1,20 +1,29 @@ require 'rails_helper' describe SearchController do - context "integration" do before do SearchIndexer.enable end + before do + # TODO be a bit more strategic here instead of junking + # all of redis + $redis.flushall + end + + after do + $redis.flushall + end + it "can search correctly" do my_post = Fabricate(:post, raw: 'this is my really awesome post') - get :query, params: { + get "/search/query.json", params: { term: 'awesome', include_blurb: true - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data['posts'][0]['id']).to eq(my_post.id) expect(data['posts'][0]['blurb']).to eq('this is my really awesome post') @@ -25,21 +34,21 @@ describe SearchController do user = Fabricate(:user) my_post = Fabricate(:post, raw: "#{user.username} is a cool person") - get :query, params: { + get "/search/query.json", params: { term: user.username, type_filter: 'topic' - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data['posts'][0]['id']).to eq(my_post.id) expect(data['users']).to be_blank - get :query, params: { + get "/search/query.json", params: { term: user.username, type_filter: 'user' - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data['posts']).to be_blank @@ -52,13 +61,13 @@ describe SearchController do post = Fabricate(:post) - get :query, params: { + get "/search/query.json", params: { term: post.topic_id, type_filter: 'topic', search_for_id: true - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data['topics'][0]['id']).to eq(post.topic_id) @@ -68,13 +77,13 @@ describe SearchController do user = Fabricate(:user) my_post = Fabricate(:post, raw: "#{user.username} is a cool person") - get :query, params: { + get "/search/query.json", params: { term: my_post.topic_id, type_filter: 'topic', search_for_id: true - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) data = JSON.parse(response.body) expect(data['topics'][0]['id']).to eq(my_post.topic_id) @@ -85,9 +94,9 @@ describe SearchController do context "#query" do it "logs the search term" do SiteSetting.log_search_queries = true - get :query, params: { term: 'wookie' }, format: :json + get "/search/query.json", params: { term: 'wookie' } - expect(response).to be_success + expect(response.status).to eq(200) expect(SearchLog.where(term: 'wookie')).to be_present json = JSON.parse(response.body) @@ -101,8 +110,8 @@ describe SearchController do it "doesn't log when disabled" do SiteSetting.log_search_queries = false - get :query, params: { term: 'wookie' }, format: :json - expect(response).to be_success + get "/search/query.json", params: { term: 'wookie' } + expect(response.status).to eq(200) expect(SearchLog.where(term: 'wookie')).to be_blank end end @@ -110,31 +119,30 @@ describe SearchController do context "#show" do it "logs the search term" do SiteSetting.log_search_queries = true - get :show, params: { q: 'bantha' }, format: :json - expect(response).to be_success + get "/search.json", params: { q: 'bantha' } + expect(response.status).to eq(200) expect(SearchLog.where(term: 'bantha')).to be_present end it "doesn't log when disabled" do SiteSetting.log_search_queries = false - get :show, params: { q: 'bantha' }, format: :json - expect(response).to be_success + get "/search.json", params: { q: 'bantha' } + expect(response.status).to eq(200) expect(SearchLog.where(term: 'bantha')).to be_blank end end context "search context" do it "raises an error with an invalid context type" do - get :query, params: { + get "/search/query.json", params: { term: 'test', search_context: { type: 'security', id: 'hole' } - }, format: :json + } expect(response.status).to eq(400) end it "raises an error with a missing id" do - get :query, - params: { term: 'test', search_context: { type: 'user' } }, - format: :json + get "/search/query.json", + params: { term: 'test', search_context: { type: 'user' } } expect(response.status).to eq(400) end @@ -142,19 +150,18 @@ describe SearchController do let(:user) { Fabricate(:user) } it "raises an error if the user can't see the context" do - Guardian.any_instance.expects(:can_see?).with(user).returns(false) - get :query, params: { - term: 'test', search_context: { type: 'user', id: user.username } - }, format: :json - expect(response).not_to be_success + get "/search/query.json", params: { + term: 'test', search_context: { type: 'private_messages', id: user.username } + } + expect(response).to be_forbidden end it 'performs the query with a search context' do - get :query, params: { + get "/search/query.json", params: { term: 'test', search_context: { type: 'user', id: user.username } - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) end end @@ -166,128 +173,128 @@ describe SearchController do end it "doesn't work wthout the necessary parameters" do - expect do - post :click, format: :json - end.to raise_error(ActionController::ParameterMissing) + post "/search/click.json" + expect(response.status).to eq(400) end it "doesn't record the click for a different user" do - log_in(:user) + sign_in(Fabricate(:user)) _, search_log_id = SearchLog.log( - term: 'kitty', + term: SecureRandom.hex, search_type: :header, user_id: -10, ip_address: '127.0.0.1' ) - post :click, params: { + post "/search/click", params: { search_log_id: search_log_id, search_result_id: 12345, search_result_type: 'topic' } - expect(response).to be_success + expect(response.status).to eq(200) expect(SearchLog.find(search_log_id).search_result_id).to be_blank end it "records the click for a logged in user" do - user = log_in(:user) + user = sign_in(Fabricate(:user)) _, search_log_id = SearchLog.log( - term: 'foobar', + term: SecureRandom.hex, search_type: :header, user_id: user.id, ip_address: '127.0.0.1' ) - post :click, params: { + post "/search/click.json", params: { search_log_id: search_log_id, search_result_id: 12345, search_result_type: 'user' - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) expect(SearchLog.find(search_log_id).search_result_id).to eq(12345) expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:user]) end it "records the click for an anonymous user" do - request.remote_addr = '192.168.0.1'; + get "/" + ip_address = request.remote_ip _, search_log_id = SearchLog.log( - term: 'kitty', + term: SecureRandom.hex, search_type: :header, - ip_address: '192.168.0.1' + ip_address: ip_address ) - post :click, params: { - search_log_id: search_log_id, - search_result_id: 22222, - search_result_type: 'topic' - }, format: :json - - expect(response).to be_success - expect(SearchLog.find(search_log_id).search_result_id).to eq(22222) - expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:topic]) - end - - it "doesn't record the click for a different IP" do - request.stubs(:remote_ip).returns('192.168.0.2') - - _, search_log_id = SearchLog.log( - term: 'kitty', - search_type: :header, - ip_address: '192.168.0.1' - ) - - post :click, params: { + post "/search/click.json", params: { search_log_id: search_log_id, search_result_id: 22222, search_result_type: 'topic' } - expect(response).to be_success + expect(response.status).to eq(200) + expect(SearchLog.find(search_log_id).search_result_id).to eq(22222) + expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:topic]) + end + + it "doesn't record the click for a different IP" do + _, search_log_id = SearchLog.log( + term: SecureRandom.hex, + search_type: :header, + ip_address: '192.168.0.19' + ) + + post "/search/click", params: { + search_log_id: search_log_id, + search_result_id: 22222, + search_result_type: 'topic' + } + + expect(response.status).to eq(200) expect(SearchLog.find(search_log_id).search_result_id).to be_blank end it "records the click for search result type category" do - request.remote_addr = '192.168.0.1'; + get "/" + ip_address = request.remote_ip _, search_log_id = SearchLog.log( - term: 'dev', + term: SecureRandom.hex, search_type: :header, - ip_address: '192.168.0.1' + ip_address: ip_address ) - post :click, params: { + post "/search/click.json", params: { search_log_id: search_log_id, search_result_id: 23456, search_result_type: 'category' - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) expect(SearchLog.find(search_log_id).search_result_id).to eq(23456) expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:category]) end it "records the click for search result type tag" do - request.remote_addr = '192.168.0.1'; - tag = Fabricate(:tag, name: 'test') + get "/" + ip_address = request.remote_ip + tag = Fabricate(:tag, name: 'test') _, search_log_id = SearchLog.log( - term: 'test', + term: SecureRandom.hex, search_type: :header, - ip_address: '192.168.0.1' + ip_address: ip_address ) - post :click, params: { + post "/search/click.json", params: { search_log_id: search_log_id, search_result_id: tag.name, search_result_type: 'tag' - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) expect(SearchLog.find(search_log_id).search_result_id).to eq(tag.id) expect(SearchLog.find(search_log_id).search_result_type).to eq(SearchLog.search_result_types[:tag]) end diff --git a/spec/requests/session_controller_spec.rb b/spec/requests/session_controller_spec.rb index 3dfb07c583..43e8a4b568 100644 --- a/spec/requests/session_controller_spec.rb +++ b/spec/requests/session_controller_spec.rb @@ -7,7 +7,7 @@ RSpec.describe SessionController do shared_examples 'failed to continue local login' do it 'should return the right response' do - expect(response).not_to be_success + expect(response).not_to be_successful expect(response.status).to eq(500) end end @@ -28,7 +28,7 @@ RSpec.describe SessionController do it 'returns the right response' do get "/session/email-login/adasdad" - expect(response).to be_success + expect(response.status).to eq(200) expect(CGI.unescapeHTML(response.body)).to match( I18n.t('email_login.invalid_token') @@ -41,7 +41,7 @@ RSpec.describe SessionController do get "/session/email-login/#{email_token.token}" - expect(response).to be_success + expect(response.status).to eq(200) expect(CGI.unescapeHTML(response.body)).to match( I18n.t('email_login.invalid_token') @@ -226,7 +226,7 @@ RSpec.describe SessionController do describe '#sso_login' do before do - @sso_url = "http://somesite.com/discourse_sso" + @sso_url = "http://example.com/discourse_sso" @sso_secret = "shjkfdhsfkjh" SiteSetting.sso_url = @sso_url @@ -630,7 +630,7 @@ RSpec.describe SessionController do expect(sso2.profile_background_url.blank?).to_not eq(true) expect(sso2.card_background_url.blank?).to_not eq(true) - expect(sso2.avatar_url).to start_with(SiteSetting.s3_cdn_url) + expect(sso2.avatar_url).to start_with("#{SiteSetting.s3_cdn_url}/original") expect(sso2.profile_background_url).to start_with(SiteSetting.s3_cdn_url) expect(sso2.card_background_url).to start_with(SiteSetting.s3_cdn_url) end @@ -774,7 +774,7 @@ RSpec.describe SessionController do expect(sso2.profile_background_url.blank?).to_not eq(true) expect(sso2.card_background_url.blank?).to_not eq(true) - expect(sso2.avatar_url).to start_with(Discourse.base_url) + expect(sso2.avatar_url).to start_with("#{Discourse.store.absolute_base_url}/original") expect(sso2.profile_background_url).to start_with(Discourse.base_url) expect(sso2.card_background_url).to start_with(Discourse.base_url) end @@ -823,7 +823,7 @@ RSpec.describe SessionController do login: user.username, password: 'sssss' } - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['error']).to eq( I18n.t("login.incorrect_username_email_or_password") ) @@ -837,7 +837,7 @@ RSpec.describe SessionController do login: user.username, password: ('s' * (User.max_password_length + 1)) } - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['error']).to eq( I18n.t("login.incorrect_username_email_or_password") ) @@ -855,7 +855,7 @@ RSpec.describe SessionController do login: user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to eq(I18n.t('login.suspended_with_reason', date: I18n.l(user.suspended_till, format: :date_only), reason: Rack::Utils.escape_html(user.suspend_reason) @@ -872,7 +872,7 @@ RSpec.describe SessionController do login: user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to eq(I18n.t('login.not_activated')) end end @@ -885,7 +885,7 @@ RSpec.describe SessionController do } end - expect(response).to be_success + expect(response.status).to eq(200) expect(events.map { |event| event[:event_name] }).to contain_exactly( :user_logged_in, :user_first_logged_in ) @@ -908,7 +908,7 @@ RSpec.describe SessionController do password: 'myawesomepassword', } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to eq(I18n.t( 'login.invalid_second_factor_code' )) @@ -923,7 +923,7 @@ RSpec.describe SessionController do second_factor_token: '00000000' } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to eq(I18n.t( 'login.invalid_second_factor_code' )) @@ -937,7 +937,7 @@ RSpec.describe SessionController do password: 'myawesomepassword', second_factor_token: ROTP::TOTP.new(user_second_factor.data).now } - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(session[:current_user_id]).to eq(user.id) @@ -957,7 +957,7 @@ RSpec.describe SessionController do post "/session.json", params: { login: "@" + user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(session[:current_user_id]).to be_nil @@ -969,7 +969,7 @@ RSpec.describe SessionController do post "/session.json", params: { login: "@" + user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(session[:current_user_id]).to eq(user.id) @@ -981,7 +981,7 @@ RSpec.describe SessionController do post "/session.json", params: { login: user.email, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(user.id) end end @@ -994,7 +994,7 @@ RSpec.describe SessionController do post "/session.json", params: { login: username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['error']).not_to be_present end @@ -1002,7 +1002,7 @@ RSpec.describe SessionController do post "/session.json", params: { login: email, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(::JSON.parse(response.body)['error']).not_to be_present end end @@ -1020,12 +1020,12 @@ RSpec.describe SessionController do end it "doesn't log in the user" do - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to be_blank end it "shows the 'not approved' error message" do - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to eq( I18n.t('login.not_approved') ) @@ -1040,7 +1040,7 @@ RSpec.describe SessionController do post "/session.json", params: { login: user.email, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(user.id) end end @@ -1063,7 +1063,7 @@ RSpec.describe SessionController do post "/session.json", params: { login: user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(user.id) end @@ -1076,7 +1076,7 @@ RSpec.describe SessionController do login: user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to be_present expect(session[:current_user_id]).not_to eq(user.id) end @@ -1090,7 +1090,7 @@ RSpec.describe SessionController do login: user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(user.id) end end @@ -1105,13 +1105,13 @@ RSpec.describe SessionController do it "doesn't log in the user" do post_login - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to be_blank end it "shows the 'not activated' error message" do post_login - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to eq( I18n.t 'login.not_activated' ) @@ -1122,7 +1122,7 @@ RSpec.describe SessionController do it "shows the 'not approved' error message" do post_login - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)['error']).to eq( I18n.t 'login.not_approved' ) @@ -1141,7 +1141,7 @@ RSpec.describe SessionController do login: user.username, password: 'myawesomepassword' } - expect(response).to be_success + expect(response.status).to eq(200) end post "/session.json", params: { @@ -1164,7 +1164,7 @@ RSpec.describe SessionController do second_factor_token: '000000' } - expect(response).to be_success + expect(response.status).to eq(200) end post "/session.json", params: { @@ -1285,7 +1285,7 @@ RSpec.describe SessionController do it "returns the JSON for the user" do get "/session/current.json" - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json['current_user']).to be_present expect(json['current_user']['id']).to eq(user.id) diff --git a/spec/requests/similar_topics_controller_spec.rb b/spec/requests/similar_topics_controller_spec.rb new file mode 100644 index 0000000000..93e234f22e --- /dev/null +++ b/spec/requests/similar_topics_controller_spec.rb @@ -0,0 +1,91 @@ +require 'rails_helper' + +describe SimilarTopicsController do + context 'similar_to' do + let(:title) { 'this title is long enough to search for' } + let(:raw) { 'this body is long enough to search for' } + + let(:topic) { Fabricate(:topic, title: title) } + let(:post) { Fabricate(:post, topic: topic, raw: raw, post_number: 1) } + + let(:private_post) { Fabricate(:post, raw: raw, topic: private_topic, post_number: 1) } + let(:private_topic) do + Fabricate(:topic, title: "#{title} 02", category: Fabricate(:private_category, group: Group[:staff])) + end + + def reindex_posts + SearchIndexer.enable + Jobs::ReindexSearch.new.rebuild_problem_posts + end + + it "requires a title param" do + get "/topics/similar_to.json", params: { raw: raw } + expect(response.status).to eq(400) + end + + it "returns no results if the title length is below the minimum" do + SiteSetting.minimum_topics_similar = 0 + SiteSetting.min_title_similar_length = 100 + post + reindex_posts + + get "/topics/similar_to.json", params: { title: title, raw: raw } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["similar_topics"].size).to eq(0) + end + + describe "minimum_topics_similar" do + before do + SiteSetting.minimum_topics_similar = 30 + end + + describe "With enough topics" do + it "deletes to Topic.similar_to if there are more topics than `minimum_topics_similar`" do + Topic.stubs(:count).returns(50) + post + reindex_posts + + get "/topics/similar_to.json", params: { title: title, raw: raw } + + expect(response.status).to eq(200) + similar_topics = ::JSON.parse(response.body)["similar_topics"] + expect(similar_topics.size).to eq(1) + expect(similar_topics.first["topic_id"]).to eq(topic.id) + end + + describe "with a logged in user" do + before do + private_post; post + reindex_posts + Topic.stubs(:count).returns(50) + sign_in(Fabricate(:moderator)) + Group.refresh_automatic_groups! + end + + it "passes a user through if logged in" do + get "/topics/similar_to.json", params: { title: title, raw: raw } + + expect(response.status).to eq(200) + similar_topics = ::JSON.parse(response.body)["similar_topics"].map { |topic| topic["topic_id"] } + expect(similar_topics.size).to eq(2) + expect(similar_topics).to include(topic.id) + expect(similar_topics).to include(private_topic.id) + end + end + end + + it "does not call Topic.similar_to if there are fewer topics than `minimum_topics_similar`" do + Topic.stubs(:count).returns(10) + post + reindex_posts + + get "/topics/similar_to.json", params: { title: title, raw: raw } + + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["similar_topics"].size).to eq(0) + end + end + end +end diff --git a/spec/controllers/site_controller_spec.rb b/spec/requests/site_controller_spec.rb similarity index 93% rename from spec/controllers/site_controller_spec.rb rename to spec/requests/site_controller_spec.rb index c36a79589c..b084dec7b1 100644 --- a/spec/controllers/site_controller_spec.rb +++ b/spec/requests/site_controller_spec.rb @@ -2,7 +2,6 @@ require 'rails_helper' describe SiteController do describe '.basic_info' do - it 'is visible always even for sites requiring login' do SiteSetting.login_required = true @@ -13,7 +12,7 @@ describe SiteController do SiteSetting.apple_touch_icon_url = "https://boom.com/apple/logo.png" SiteSetting.mobile_logo_url = "https://a.a/a.png" - get :basic_info, format: :json + get "/site/basic-info.json" json = JSON.parse(response.body) expect(json["title"]).to eq("Hammer Time") @@ -26,15 +25,14 @@ describe SiteController do end describe '.statistics' do - it 'is visible for sites requiring login' do SiteSetting.login_required = true SiteSetting.share_anonymized_statistics = true - get :statistics, format: :json + get "/site/statistics.json" json = JSON.parse(response.body) - expect(response).to be_success + expect(response.status).to eq(200) expect(json["topic_count"]).to be_present expect(json["post_count"]).to be_present expect(json["user_count"]).to be_present @@ -54,7 +52,7 @@ describe SiteController do it 'is not visible if site setting share_anonymized_statistics is disabled' do SiteSetting.share_anonymized_statistics = false - get :statistics, format: :json + get "/site/statistics.json" expect(response).to redirect_to '/' end end diff --git a/spec/requests/static_controller_spec.rb b/spec/requests/static_controller_spec.rb index 624bdadb2c..490d2966dc 100644 --- a/spec/requests/static_controller_spec.rb +++ b/spec/requests/static_controller_spec.rb @@ -98,7 +98,7 @@ describe StaticController do it "should return the right response" do get "/faq" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(I18n.t('js.faq')) end end @@ -113,7 +113,7 @@ describe StaticController do it "renders the #{id} page" do get "/#{id}" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(text) end end @@ -150,7 +150,7 @@ describe StaticController do get "/login" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(PrettyText.cook(I18n.t( 'login_required.welcome_message', title: SiteSetting.title @@ -177,7 +177,7 @@ describe StaticController do get '/faq' - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(I18n.t('js.faq')) end @@ -186,7 +186,7 @@ describe StaticController do get '/guidelines' - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(I18n.t('guidelines')) end end diff --git a/spec/requests/steps_controller_spec.rb b/spec/requests/steps_controller_spec.rb new file mode 100644 index 0000000000..6f304bf70a --- /dev/null +++ b/spec/requests/steps_controller_spec.rb @@ -0,0 +1,55 @@ +require 'rails_helper' + +describe StepsController do + before do + SiteSetting.wizard_enabled = true + end + + it 'needs you to be logged in' do + put "/wizard/steps/made-up-id.json", params: { + fields: { forum_title: "updated title" } + } + expect(response.status).to eq(403) + end + + it "raises an error if you aren't an admin" do + sign_in(Fabricate(:moderator)) + + put "/wizard/steps/made-up-id.json", params: { + fields: { forum_title: "updated title" } + } + + expect(response).to be_forbidden + end + + context "as an admin" do + before do + sign_in(Fabricate(:admin)) + end + + it "raises an error if the wizard is disabled" do + SiteSetting.wizard_enabled = false + put "/wizard/steps/contact.json", params: { + fields: { contact_email: "eviltrout@example.com" } + } + expect(response).to be_forbidden + end + + it "updates properly if you are staff" do + put "/wizard/steps/contact.json", params: { + fields: { contact_email: "eviltrout@example.com" } + } + + expect(response.status).to eq(200) + expect(SiteSetting.contact_email).to eq("eviltrout@example.com") + end + + it "returns errors if the field has them" do + put "/wizard/steps/contact.json", params: { + fields: { contact_email: "not-an-email" } + } + + expect(response.status).to eq(422) + end + end +end diff --git a/spec/controllers/stylesheets_controller_spec.rb b/spec/requests/stylesheets_controller_spec.rb similarity index 57% rename from spec/controllers/stylesheets_controller_spec.rb rename to spec/requests/stylesheets_controller_spec.rb index a68ccc62be..d33961fa75 100644 --- a/spec/controllers/stylesheets_controller_spec.rb +++ b/spec/requests/stylesheets_controller_spec.rb @@ -1,9 +1,7 @@ require 'rails_helper' describe StylesheetsController do - it 'can survive cache miss' do - StylesheetCache.destroy_all builder = Stylesheet::Manager.new('desktop_rtl', nil) builder.compile @@ -11,8 +9,8 @@ describe StylesheetsController do digest = StylesheetCache.first.digest StylesheetCache.destroy_all - get :show, params: { name: "desktop_rtl_#{digest}" }, format: :json - expect(response).to be_success + get "/stylesheets/desktop_rtl_#{digest}.css" + expect(response.status).to eq(200) cached = StylesheetCache.first expect(cached.target).to eq 'desktop_rtl' @@ -21,11 +19,10 @@ describe StylesheetsController do # tmp folder destruction and cached `rm #{Stylesheet::Manager.cache_fullpath}/*` - get :show, params: { name: "desktop_rtl_#{digest}" }, format: :json - expect(response).to be_success + get "/stylesheets/desktop_rtl_#{digest}.css" + expect(response.status).to eq(200) # there is an edge case which is ... disk and db cache is nuked, very unlikely to happen - end it 'can lookup theme specific css' do @@ -37,34 +34,25 @@ describe StylesheetsController do `rm #{Stylesheet::Manager.cache_fullpath}/*` - get :show, params: { - name: builder.stylesheet_filename.sub(".css", "") - }, format: :json + get "/stylesheets/#{builder.stylesheet_filename.sub(".css", "")}.css" - expect(response).to be_success + expect(response.status).to eq(200) - get :show, params: { - name: builder.stylesheet_filename_no_digest.sub(".css", "") - }, format: :json + get "/stylesheets/#{builder.stylesheet_filename_no_digest.sub(".css", "")}.css" - expect(response).to be_success + expect(response.status).to eq(200) builder = Stylesheet::Manager.new(:desktop_theme, theme.key) builder.compile `rm #{Stylesheet::Manager.cache_fullpath}/*` - get :show, params: { - name: builder.stylesheet_filename.sub(".css", "") - }, format: :json + get "/stylesheets/#{builder.stylesheet_filename.sub(".css", "")}.css" - expect(response).to be_success + expect(response.status).to eq(200) - get :show, params: { - name: builder.stylesheet_filename_no_digest.sub(".css", "") - }, format: :json + get "/stylesheets/#{builder.stylesheet_filename_no_digest.sub(".css", "")}.css" - expect(response).to be_success + expect(response.status).to eq(200) end - end diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb index 65165852a9..9be7b71022 100644 --- a/spec/requests/tags_controller_spec.rb +++ b/spec/requests/tags_controller_spec.rb @@ -16,7 +16,7 @@ describe TagsController do it "should return the right response" do get "/tags.json" - expect(response).to be_success + expect(response.status).to eq(200) tags = JSON.parse(response.body)["tags"] expect(tags.length).to eq(1) @@ -41,7 +41,7 @@ describe TagsController do get "/tags.json" - expect(response).to be_success + expect(response.status).to eq(200) tags = JSON.parse(response.body)["tags"] expect(tags.length).to eq(2) @@ -57,7 +57,7 @@ describe TagsController do it "should return the right response" do get "/tags/test" - expect(response).to be_success + expect(response.status).to eq(200) end it "should handle invalid tags" do @@ -72,7 +72,7 @@ describe TagsController do it "should return the right response" do get "/tags/check.json", params: { tag_values: [tag.name] } - expect(response).to be_success + expect(response.status).to eq(200) tag = JSON.parse(response.body)["valid"].first expect(tag["value"]).to eq('test') @@ -123,7 +123,7 @@ describe TagsController do it "can't see pm tags" do get "/tags/personal_messages/#{regular_user.username}.json" - expect(response).not_to be_success + expect(response).not_to be_successful end end @@ -135,13 +135,13 @@ describe TagsController do it "can't see pm tags for regular user" do get "/tags/personal_messages/#{regular_user.username}.json" - expect(response).not_to be_success + expect(response).not_to be_successful end it "can see their own pm tags" do get "/tags/personal_messages/#{moderator.username}.json" - expect(response).to be_success + expect(response.status).to eq(200) tag = JSON.parse(response.body)['tags'] expect(tag[0]["id"]).to eq('test') @@ -156,7 +156,7 @@ describe TagsController do it "can see pm tags for regular user" do get "/tags/personal_messages/#{regular_user.username}.json" - expect(response).to be_success + expect(response.status).to eq(200) tag = JSON.parse(response.body)['tags'] expect(tag[0]["id"]).to eq('test') @@ -165,11 +165,208 @@ describe TagsController do it "can see their own pm tags" do get "/tags/personal_messages/#{admin.username}.json" - expect(response).to be_success + expect(response.status).to eq(200) tag = JSON.parse(response.body)['tags'] expect(tag[0]["id"]).to eq('test') end end end + + describe '#show_latest' do + let(:tag) { Fabricate(:tag) } + let(:other_tag) { Fabricate(:tag) } + let(:third_tag) { Fabricate(:tag) } + let(:category) { Fabricate(:category) } + let(:subcategory) { Fabricate(:category, parent_category_id: category.id) } + + let(:single_tag_topic) { Fabricate(:topic, tags: [tag]) } + let(:multi_tag_topic) { Fabricate(:topic, tags: [tag, other_tag]) } + let(:all_tag_topic) { Fabricate(:topic, tags: [tag, other_tag, third_tag]) } + + context 'tagging disabled' do + it "returns 404" do + SiteSetting.tagging_enabled = false + get "/tags/#{tag.name}/l/latest.json" + expect(response.status).to eq(404) + end + end + + context 'tagging enabled' do + it "can filter by tag" do + get "/tags/#{tag.name}/l/latest.json" + expect(response.status).to eq(200) + end + + it "can filter by two tags" do + single_tag_topic; multi_tag_topic; all_tag_topic + + get "/tags/#{tag.name}/l/latest.json", params: { + additional_tag_ids: other_tag.name + } + + expect(response.status).to eq(200) + + topic_ids = JSON.parse(response.body)["topic_list"]["topics"] + .map { |topic| topic["id"] } + + expect(topic_ids).to include(all_tag_topic.id) + expect(topic_ids).to include(multi_tag_topic.id) + expect(topic_ids).to_not include(single_tag_topic.id) + end + + it "can filter by multiple tags" do + single_tag_topic; multi_tag_topic; all_tag_topic + + get "/tags/#{tag.name}/l/latest.json", params: { + additional_tag_ids: "#{other_tag.name}/#{third_tag.name}" + } + + expect(response.status).to eq(200) + + topic_ids = JSON.parse(response.body)["topic_list"]["topics"] + .map { |topic| topic["id"] } + + expect(topic_ids).to include(all_tag_topic.id) + expect(topic_ids).to_not include(multi_tag_topic.id) + expect(topic_ids).to_not include(single_tag_topic.id) + end + + it "does not find any tags when a tag which doesn't exist is passed" do + single_tag_topic + + get "/tags/#{tag.name}/l/latest.json", params: { + additional_tag_ids: "notatag" + } + + expect(response.status).to eq(200) + + topic_ids = JSON.parse(response.body)["topic_list"]["topics"] + .map { |topic| topic["id"] } + + expect(topic_ids).to_not include(single_tag_topic.id) + end + + it "can filter by category and tag" do + get "/tags/c/#{category.slug}/#{tag.name}/l/latest.json" + expect(response.status).to eq(200) + end + + it "can filter by category, sub-category, and tag" do + get "/tags/c/#{category.slug}/#{subcategory.slug}/#{tag.name}/l/latest.json" + expect(response.status).to eq(200) + end + + it "can filter by category, no sub-category, and tag" do + get "/tags/c/#{category.slug}/none/#{tag.name}/l/latest.json" + expect(response.status).to eq(200) + end + + it "can handle subcategories with the same name" do + category2 = Fabricate(:category) + subcategory2 = Fabricate(:category, + parent_category_id: category2.id, + name: subcategory.name, + slug: subcategory.slug + ) + t = Fabricate(:topic, category_id: subcategory2.id, tags: [other_tag]) + get "/tags/c/#{category2.slug}/#{subcategory2.slug}/#{other_tag.name}/l/latest.json" + + expect(response.status).to eq(200) + + topic_ids = JSON.parse(response.body)["topic_list"]["topics"] + .map { |topic| topic["id"] } + + expect(topic_ids).to include(t.id) + end + + it "can filter by bookmarked" do + sign_in(Fabricate(:user)) + get "/tags/#{tag.name}/l/bookmarks.json" + + expect(response.status).to eq(200) + end + end + end + + describe '#search' do + context 'tagging disabled' do + it "returns 404" do + SiteSetting.tagging_enabled = false + get "/tags/filter/search.json", params: { q: 'stuff' } + expect(response.status).to eq(404) + end + end + + context 'tagging enabled' do + it "can return some tags" do + tag_names = ['stuff', 'stinky', 'stumped'] + tag_names.each { |name| Fabricate(:tag, name: name) } + get "/tags/filter/search.json", params: { q: 'stu' } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["results"].map { |j| j["id"] }.sort).to eq(['stuff', 'stumped']) + end + + it "can say if given tag is not allowed" do + yup, nope = Fabricate(:tag, name: 'yup'), Fabricate(:tag, name: 'nope') + category = Fabricate(:category, tags: [yup]) + get "/tags/filter/search.json", params: { q: 'nope', categoryId: category.id } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["results"].map { |j| j["id"] }.sort).to eq([]) + expect(json["forbidden"]).to be_present + end + + it "can return tags that are in secured categories but are allowed to be used" do + c = Fabricate(:private_category, group: Fabricate(:group)) + Fabricate(:topic, category: c, tags: [Fabricate(:tag, name: "cooltag")]) + get "/tags/filter/search.json", params: { q: "cool" } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["results"].map { |j| j["id"] }).to eq(['cooltag']) + end + + it "supports Chinese and Russian" do + tag_names = ['房地产', 'тема-в-разработке'] + tag_names.each { |name| Fabricate(:tag, name: name) } + + get "/tags/filter/search.json", params: { q: '房' } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["results"].map { |j| j["id"] }).to eq(['房地产']) + + get "/tags/filter/search.json", params: { q: 'тема' } + expect(response.status).to eq(200) + json = ::JSON.parse(response.body) + expect(json["results"].map { |j| j["id"] }).to eq(['тема-в-разработке']) + end + end + end + + describe '#destroy' do + context 'tagging enabled' do + before do + sign_in(Fabricate(:admin)) + end + + context 'with an existent tag name' do + it 'deletes the tag' do + tag = Fabricate(:tag) + delete "/tags/#{tag.name}.json" + expect(response.status).to eq(200) + expect(Tag.where(id: tag.id)).to be_empty + end + end + + context 'with a nonexistent tag name' do + it 'returns a tag not found message' do + delete "/tags/doesntexists.json" + expect(response).not_to be_successful + json = ::JSON.parse(response.body) + expect(json['error_type']).to eq('not_found') + end + end + end + end end diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index 597ced4be5..39b8e3288e 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -15,7 +15,7 @@ RSpec.describe TopicsController do get "/t/#{topic.id}/wordpress.json", params: { best: 3 } - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) # The JSON has the data the wordpress plugin needs @@ -105,7 +105,7 @@ RSpec.describe TopicsController do } end.to change { Topic.count }.by(1) - expect(response).to be_success + expect(response.status).to eq(200) result = ::JSON.parse(response.body) @@ -127,7 +127,7 @@ RSpec.describe TopicsController do } end.to change { Topic.count }.by(1) - expect(response).to be_success + expect(response.status).to eq(200) result = JSON.parse(response.body) @@ -143,7 +143,7 @@ RSpec.describe TopicsController do post "/t/#{topic.id}/move-posts.json", params: { post_ids: [p2.id] } - expect(response).to be_success + expect(response.status).to eq(200) result = ::JSON.parse(response.body) expect(result['success']).to eq(false) expect(result['url']).to be_blank @@ -193,7 +193,7 @@ RSpec.describe TopicsController do destination_topic_id: dest_topic.id } - expect(response).to be_success + expect(response.status).to eq(200) result = ::JSON.parse(response.body) expect(result['success']).to eq(true) expect(result['url']).to be_present @@ -207,7 +207,7 @@ RSpec.describe TopicsController do post_ids: [p2.id] } - expect(response).to be_success + expect(response.status).to eq(200) result = ::JSON.parse(response.body) expect(result['success']).to eq(false) expect(result['url']).to be_blank @@ -251,7 +251,7 @@ RSpec.describe TopicsController do destination_topic_id: dest_topic.id } - expect(response).to be_success + expect(response.status).to eq(200) result = ::JSON.parse(response.body) expect(result['success']).to eq(true) expect(result['url']).to be_present @@ -298,8 +298,8 @@ RSpec.describe TopicsController do let!(:editor) { sign_in(Fabricate(:admin)) } let(:topic) { Fabricate(:topic) } let(:user_a) { Fabricate(:user) } - let(:p1) { Fabricate(:post, topic_id: topic.id) } - let(:p2) { Fabricate(:post, topic_id: topic.id) } + let(:p1) { Fabricate(:post, topic: topic) } + let(:p2) { Fabricate(:post, topic: topic) } it "raises an error with a parameter missing" do [ @@ -317,7 +317,7 @@ RSpec.describe TopicsController do } topic.reload p1.reload - expect(response).to be_success + expect(response.status).to eq(200) expect(topic.user.username).to eq(user_a.username) expect(p1.user.username).to eq(user_a.username) end @@ -327,7 +327,7 @@ RSpec.describe TopicsController do username: user_a.username_lower, post_ids: [p1.id, p2.id] } - expect(response).to be_success + expect(response.status).to eq(200) p1.reload p2.reload @@ -339,7 +339,7 @@ RSpec.describe TopicsController do it "works with deleted users" do deleted_user = Fabricate(:user) t2 = Fabricate(:topic, user: deleted_user) - p3 = Fabricate(:post, topic_id: t2.id, user: deleted_user) + p3 = Fabricate(:post, topic: t2, user: deleted_user) UserDestroyer.new(editor).destroy(deleted_user, delete_posts: true, context: 'test', delete_as_spammer: true) @@ -347,7 +347,7 @@ RSpec.describe TopicsController do username: user_a.username_lower, post_ids: [p3.id] } - expect(response).to be_success + expect(response.status).to eq(200) t2.reload p3.reload expect(t2.deleted_at).to be_nil @@ -380,20 +380,19 @@ RSpec.describe TopicsController do let(:old_timestamp) { Time.zone.now } let(:new_timestamp) { old_timestamp - 1.day } let!(:topic) { Fabricate(:topic, created_at: old_timestamp) } - let!(:p1) { Fabricate(:post, topic_id: topic.id, created_at: old_timestamp) } - let!(:p2) { Fabricate(:post, topic_id: topic.id, created_at: old_timestamp + 1.day) } - - it 'raises an error with a missing parameter' do - put "/t/1/change-timestamp.json" - expect(response.status).to eq(400) - end + let!(:p1) { Fabricate(:post, topic: topic, created_at: old_timestamp) } + let!(:p2) { Fabricate(:post, topic: topic, created_at: old_timestamp + 1.day) } it 'should update the timestamps of selected posts' do + # try to see if we fail with invalid first + put "/t/1/change-timestamp.json" + expect(response.status).to eq(400) + put "/t/#{topic.id}/change-timestamp.json", params: { timestamp: new_timestamp.to_f } - expect(response).to be_success + expect(response.status).to eq(200) expect(topic.reload.created_at).to be_within_one_second_of(new_timestamp) expect(p1.reload.created_at).to be_within_one_second_of(new_timestamp) expect(p2.reload.created_at).to be_within_one_second_of(old_timestamp) @@ -425,7 +424,7 @@ RSpec.describe TopicsController do expect do put "/t/#{topic.id}/clear-pin.json" end.to change { TopicUser.where(topic_id: topic.id, user_id: user.id).count }.by(1) - expect(response).to be_success + expect(response.status).to eq(200) end end end @@ -473,15 +472,14 @@ RSpec.describe TopicsController do end it 'should update the status of the topic correctly' do - topic = Fabricate(:topic, user: user, closed: true, topic_timers: [ - Fabricate(:topic_timer, status_type: TopicTimer.types[:open]) - ]) + topic = Fabricate(:topic, user: user, closed: true) + Fabricate(:topic_timer, topic: topic, status_type: TopicTimer.types[:open]) put "/t/#{topic.id}/status.json", params: { status: 'closed', enabled: 'false' } - expect(response).to be_success + expect(response.status).to eq(200) expect(topic.reload.closed).to eq(false) expect(topic.topic_timers).to eq([]) @@ -562,7 +560,7 @@ RSpec.describe TopicsController do put "/t/#{topic.id}/recover.json" topic.reload post.reload - expect(response).to be_success + expect(response.status).to eq(200) expect(topic.trashed?).to be_falsey expect(post.trashed?).to be_falsey end @@ -597,7 +595,7 @@ RSpec.describe TopicsController do it 'succeeds' do delete "/t/#{topic.id}.json" - expect(response).to be_success + expect(response.status).to eq(200) topic.reload expect(topic.trashed?).to be_truthy end @@ -611,7 +609,7 @@ RSpec.describe TopicsController do it "returns JSON for the slug" do get "/t/id_for/#{topic.slug}.json" - expect(response).to be_success + expect(response.status).to eq(200) json = ::JSON.parse(response.body) expect(json['topic_id']).to eq(topic.id) expect(json['url']).to eq(topic.url) @@ -787,9 +785,9 @@ RSpec.describe TopicsController do it 'correctly renders canoicals' do get "/t/#{topic.id}", params: { slug: topic.slug } - expect(response).to be_success + expect(response.status).to eq(200) expect(css_select("link[rel=canonical]").length).to eq(1) - expect(response.headers["Cache-Control"]).to eq("no-store, must-revalidate, no-cache, private") + expect(response.headers["Cache-Control"]).to eq("no-cache, no-store") end it 'returns 301 even if slug does not match URL' do @@ -804,7 +802,7 @@ RSpec.describe TopicsController do Fabricate(:post, topic: topic) get "/t/#{topic.id}.json", params: { slug: topic.slug } - expect(response).to be_success + expect(response.status).to eq(200) get "/t/#{topic.id}.json", params: { slug: "just-guessing" } expect(response.status).to eq(301) @@ -815,7 +813,7 @@ RSpec.describe TopicsController do it 'shows a topic correctly' do get "/t/#{topic.slug}/#{topic.id}.json" - expect(response).to be_success + expect(response.status).to eq(200) end it 'return 404 for an invalid page' do @@ -1042,10 +1040,23 @@ RSpec.describe TopicsController do it 'renders the print view when enabled' do SiteSetting.max_prints_per_hour_per_user = 10 + get "/t/#{topic.slug}/#{topic.id}/print", headers: { HTTP_USER_AGENT: "Rails Testing" } - get "/t/#{topic.slug}/#{topic.id}/print" + expect(response.status).to eq(200) + body = response.body - expect(response).to be_successful + expect(body).to have_tag(:body, class: 'crawler') + expect(body).to_not have_tag(:meta, with: { name: 'fragment' }) + end + + it "uses the application layout when there's no param" do + SiteSetting.max_prints_per_hour_per_user = 10 + get "/t/#{topic.slug}/#{topic.id}", headers: { HTTP_USER_AGENT: "Rails Testing" } + + body = response.body + + expect(body).to have_tag(:script, src: '/assets/application.js') + expect(body).to have_tag(:meta, with: { name: 'fragment' }) end end @@ -1097,20 +1108,20 @@ RSpec.describe TopicsController do it 'grabs the correct set of posts' do get "/t/#{topic.slug}/#{topic.id}.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(extract_post_stream).to eq(@post_ids[0..1]) get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 1 } - expect(response).to be_success + expect(response.status).to eq(200) expect(extract_post_stream).to eq(@post_ids[0..1]) get "/t/#{topic.slug}/#{topic.id}.json", params: { page: 2 } - expect(response).to be_success + expect(response.status).to eq(200) expect(extract_post_stream).to eq(@post_ids[2..3]) post_number = topic.posts.pluck(:post_number).sort[3] get "/t/#{topic.slug}/#{topic.id}/#{post_number}.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(extract_post_stream).to eq(@post_ids[-2..-1]) end end @@ -1123,7 +1134,7 @@ RSpec.describe TopicsController do it 'shows the topic' do get "/t/#{topic.slug}/#{topic.id}.json" - expect(response).to be_successful + expect(response.status).to eq(200) end end @@ -1139,7 +1150,7 @@ RSpec.describe TopicsController do it 'shows the topic if valid api key is provided' do get "/t/#{topic.slug}/#{topic.id}.json", params: { api_key: api_key.key } - expect(response).to be_successful + expect(response.status).to eq(200) topic.reload expect(topic.views).to eq(1) end @@ -1168,6 +1179,236 @@ RSpec.describe TopicsController do expect(response.headers['X-Robots-Tag']).to eq(nil) end + + describe "themes" do + let(:theme) { Theme.create!(user_id: -1, name: 'bob', user_selectable: true) } + let(:theme2) { Theme.create!(user_id: -1, name: 'bobbob', user_selectable: true) } + + before do + sign_in(user) + end + + it "selects the theme the user has selected" do + user.user_option.update_columns(theme_key: theme.key) + + get "/t/#{topic.id}" + expect(response).to be_redirect + expect(controller.theme_key).to eq(theme.key) + + theme.update_attribute(:user_selectable, false) + + get "/t/#{topic.id}" + expect(response).to be_redirect + expect(controller.theme_key).not_to eq(theme.key) + end + + it "can be overridden with a cookie" do + user.user_option.update_columns(theme_key: theme.key) + + cookies['theme_key'] = "#{theme2.key},#{user.user_option.theme_key_seq}" + + get "/t/#{topic.id}" + expect(response).to be_redirect + expect(controller.theme_key).to eq(theme2.key) + end + + it "cookie can fail back to user if out of sync" do + user.user_option.update_columns(theme_key: theme.key) + cookies['theme_key'] = "#{theme2.key},#{user.user_option.theme_key_seq - 1}" + + get "/t/#{topic.id}" + expect(response).to be_redirect + expect(controller.theme_key).to eq(theme.key) + end + end + + it "doesn't store an incoming link when there's no referer" do + expect { + get "/t/#{topic.id}.json" + }.not_to change(IncomingLink, :count) + expect(response.status).to eq(200) + end + + it "doesn't raise an error on a very long link" do + get "/t/#{topic.id}.json", headers: { HTTP_REFERER: "http://#{'a' * 2000}.com" } + expect(response.status).to eq(200) + end + + describe "has_escaped_fragment?" do + context "when the SiteSetting is disabled" do + it "uses the application layout even with an escaped fragment param" do + SiteSetting.enable_escaped_fragments = false + + get "/t/#{topic.slug}/#{topic.id}", params: { + _escaped_fragment_: 'true' + } + + body = response.body + + expect(response.status).to eq(200) + expect(body).to have_tag(:script, with: { src: '/assets/application.js' }) + expect(body).to_not have_tag(:meta, with: { name: 'fragment' }) + end + end + + context "when the SiteSetting is enabled" do + before do + SiteSetting.enable_escaped_fragments = true + end + + it "uses the application layout when there's no param" do + get "/t/#{topic.slug}/#{topic.id}" + + body = response.body + + expect(body).to have_tag(:script, with: { src: '/assets/application.js' }) + expect(body).to have_tag(:meta, with: { name: 'fragment' }) + end + + it "uses the crawler layout when there's an _escaped_fragment_ param" do + get "/t/#{topic.slug}/#{topic.id}", params: { + _escaped_fragment_: true + }, headers: { HTTP_USER_AGENT: "Rails Testing" } + + body = response.body + + expect(response.status).to eq(200) + expect(body).to have_tag(:body, with: { class: 'crawler' }) + expect(body).to_not have_tag(:meta, with: { name: 'fragment' }) + end + end + end + + describe 'clear_notifications' do + it 'correctly clears notifications if specified via cookie' do + notification = Fabricate(:notification) + sign_in(notification.user) + + cookies['cn'] = "2828,100,#{notification.id}" + + get "/t/#{topic.id}.json" + + expect(response.status).to eq(200) + expect(response.cookies['cn']).to eq(nil) + + notification.reload + expect(notification.read).to eq(true) + end + + it 'correctly clears notifications if specified via header' do + notification = Fabricate(:notification) + sign_in(notification.user) + + get "/t/#{topic.id}.json", headers: { "Discourse-Clear-Notifications" => "2828,100,#{notification.id}" } + + expect(response.status).to eq(200) + notification.reload + expect(notification.read).to eq(true) + end + end + + describe "set_locale" do + def headers(locale) + { HTTP_ACCEPT_LANGUAGE: locale } + end + + context "allow_user_locale disabled" do + context "accept-language header differs from default locale" do + before do + SiteSetting.allow_user_locale = false + SiteSetting.default_locale = "en" + end + + context "with an anonymous user" do + it "uses the default locale" do + get "/t/#{topic.id}.json", headers: headers("fr") + + expect(response.status).to eq(200) + expect(I18n.locale).to eq(:en) + end + end + + context "with a logged in user" do + it "it uses the default locale" do + user = Fabricate(:user, locale: :fr) + sign_in(user) + + get "/t/#{topic.id}.json", headers: headers("fr") + + expect(response.status).to eq(200) + expect(I18n.locale).to eq(:en) + end + end + end + end + + context "set_locale_from_accept_language_header enabled" do + context "accept-language header differs from default locale" do + before do + SiteSetting.allow_user_locale = true + SiteSetting.set_locale_from_accept_language_header = true + SiteSetting.default_locale = "en" + end + + context "with an anonymous user" do + it "uses the locale from the headers" do + get "/t/#{topic.id}.json", headers: headers("fr") + expect(response.status).to eq(200) + expect(I18n.locale).to eq(:fr) + end + end + + context "with a logged in user" do + it "uses the user's preferred locale" do + user = Fabricate(:user, locale: :fr) + sign_in(user) + + get "/t/#{topic.id}.json", headers: headers("fr") + expect(response.status).to eq(200) + expect(I18n.locale).to eq(:fr) + end + end + end + + context "the preferred locale includes a region" do + it "returns the locale and region separated by an underscore" do + SiteSetting.allow_user_locale = true + SiteSetting.set_locale_from_accept_language_header = true + SiteSetting.default_locale = "en" + + get "/t/#{topic.id}.json", headers: headers("zh-CN") + expect(response.status).to eq(200) + expect(I18n.locale).to eq(:zh_CN) + end + end + + context 'accept-language header is not set' do + it 'uses the site default locale' do + SiteSetting.allow_user_locale = true + SiteSetting.default_locale = 'en' + + get "/t/#{topic.id}.json", headers: headers("") + expect(response.status).to eq(200) + expect(I18n.locale).to eq(:en) + end + end + end + end + + describe "read only header" do + it "returns no read only header by default" do + get "/t/#{topic.id}.json" + expect(response.status).to eq(200) + expect(response.headers['Discourse-Readonly']).to eq(nil) + end + + it "returns a readonly header if the site is read only" do + Discourse.received_readonly! + get "/t/#{topic.id}.json" + expect(response.status).to eq(200) + expect(response.headers['Discourse-Readonly']).to eq('true') + end + end end describe '#posts' do @@ -1175,7 +1416,7 @@ RSpec.describe TopicsController do it 'returns first posts of the topic' do get "/t/#{topic.id}/posts.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.content_type).to eq('application/json') end end @@ -1185,7 +1426,7 @@ RSpec.describe TopicsController do it 'renders rss of the topic' do get "/t/foo/#{topic.id}.rss" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.content_type).to eq('application/rss+xml') end end @@ -1232,7 +1473,7 @@ RSpec.describe TopicsController do topic = Fabricate(:topic, user: sign_in(Fabricate(:admin))) put "/t/#{topic.id}/make-banner.json" - expect(response).to be_success + expect(response.status).to eq(200) topic.reload expect(topic.archetype).to eq(Archetype.banner) end @@ -1251,7 +1492,7 @@ RSpec.describe TopicsController do topic = Fabricate(:topic, user: sign_in(Fabricate(:admin)), archetype: Archetype.banner) put "/t/#{topic.id}/remove-banner.json" - expect(response).to be_success + expect(response.status).to eq(200) topic.reload expect(topic.archetype).to eq(Archetype.default) end @@ -1360,7 +1601,7 @@ RSpec.describe TopicsController do user.user_stat.update_column(:new_since, old_date) put "/topics/reset-new.json" - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(user.user_stat.new_since.to_date).not_to eq(old_date.to_date) end @@ -1370,7 +1611,7 @@ RSpec.describe TopicsController do it "works" do get "/topics/feature_stats.json", params: { category_id: 1 } - expect(response).to be_success + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json["pinned_in_category_count"]).to eq(0) expect(json["pinned_globally_count"]).to eq(0) @@ -1430,12 +1671,12 @@ RSpec.describe TopicsController do context "success" do it "returns success" do - admin = sign_in(Fabricate(:admin)) + sign_in(Fabricate(:admin)) put "/t/#{topic.id}/convert-topic/private.json" topic.reload expect(topic.archetype).to eq(Archetype.private_message) - expect(response).to be_success + expect(response.status).to eq(200) result = ::JSON.parse(response.body) expect(result['success']).to eq(true) @@ -1456,12 +1697,12 @@ RSpec.describe TopicsController do context "success" do it "returns success" do - admin = sign_in(Fabricate(:admin)) + sign_in(Fabricate(:admin)) put "/t/#{topic.id}/convert-topic/public.json" topic.reload expect(topic.archetype).to eq(Archetype.default) - expect(response).to be_success + expect(response.status).to eq(200) result = ::JSON.parse(response.body) expect(result['success']).to eq(true) @@ -1483,7 +1724,7 @@ RSpec.describe TopicsController do timings: { post_1.post_number => 2 } } - expect(response).to be_success + expect(response.status).to eq(200) post_timing = PostTiming.first @@ -1531,7 +1772,7 @@ RSpec.describe TopicsController do status_type: TopicTimer.types[1] } - expect(response).to be_success + expect(response.status).to eq(200) topic_status_update = TopicTimer.last @@ -1557,7 +1798,7 @@ RSpec.describe TopicsController do status_type: TopicTimer.types[1] } - expect(response).to be_success + expect(response.status).to eq(200) expect(topic.reload.public_topic_timer).to eq(nil) json = JSON.parse(response.body) @@ -1575,7 +1816,7 @@ RSpec.describe TopicsController do category_id: topic.category_id } - expect(response).to be_success + expect(response.status).to eq(200) topic_status_update = TopicTimer.last @@ -1795,7 +2036,7 @@ RSpec.describe TopicsController do let!(:shared_draft) { Fabricate(:shared_draft, topic: topic, category: category) } it "allows staff to update the category id" do put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id } - expect(response).to be_success + expect(response.status).to eq(200) topic.reload expect(topic.shared_draft.category_id).to eq(other_cat.id) end @@ -1804,7 +2045,7 @@ RSpec.describe TopicsController do context "without a shared draft" do it "allows staff to update the category id" do put "/t/#{topic.id}/shared-draft.json", params: { category_id: other_cat.id } - expect(response).to be_success + expect(response.status).to eq(200) topic.reload expect(topic.shared_draft.category_id).to eq(other_cat.id) end @@ -1870,15 +2111,15 @@ RSpec.describe TopicsController do freeze_time page1_time topic = Fabricate(:topic) - Fabricate(:post, topic_id: topic.id) - Fabricate(:post, topic_id: topic.id) + Fabricate(:post, topic: topic) + Fabricate(:post, topic: topic) freeze_time page2_time - Fabricate(:post, topic_id: topic.id) - Fabricate(:post, topic_id: topic.id) + Fabricate(:post, topic: topic) + Fabricate(:post, topic: topic) freeze_time page3_time - Fabricate(:post, topic_id: topic.id) + Fabricate(:post, topic: topic) # ugly, but no inteface to set this and we don't want to create # 100 posts to test this thing diff --git a/spec/requests/uploads_controller_spec.rb b/spec/requests/uploads_controller_spec.rb new file mode 100644 index 0000000000..fac1a98d8d --- /dev/null +++ b/spec/requests/uploads_controller_spec.rb @@ -0,0 +1,242 @@ +require 'rails_helper' + +describe UploadsController do + describe '#create' do + it 'requires you to be logged in' do + post "/uploads.json" + expect(response.status).to eq(403) + end + + context 'logged in' do + let!(:user) { sign_in(Fabricate(:user)) } + + let(:logo) do + Rack::Test::UploadedFile.new(file_from_fixtures("logo.png")) + end + + let(:fake_jpg) do + Rack::Test::UploadedFile.new(file_from_fixtures("fake.jpg")) + end + + let(:text_file) do + Rack::Test::UploadedFile.new(File.new("#{Rails.root}/LICENSE.txt")) + end + + it 'expects a type' do + post "/uploads.json", params: { file: logo } + expect(response.status).to eq(400) + end + + it 'is successful with an image' do + post "/uploads.json", params: { file: logo, type: "avatar" } + expect(response.status).to eq 200 + expect(JSON.parse(response.body)["id"]).to be_present + expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(1) + end + + it 'is successful with an attachment' do + SiteSetting.authorized_extensions = "*" + + post "/uploads.json", params: { file: text_file, type: "composer" } + expect(response.status).to eq 200 + + expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0) + id = JSON.parse(response.body)["id"] + expect(id).to be + end + + it 'is successful with api' do + SiteSetting.authorized_extensions = "*" + api_key = Fabricate(:api_key, user: user).key + + url = "http://example.com/image.png" + png = File.read(Rails.root + "spec/fixtures/images/logo.png") + + stub_request(:get, url).to_return(status: 200, body: png) + + post "/uploads.json", params: { url: url, type: "avatar", api_key: api_key, api_username: user.username } + + json = ::JSON.parse(response.body) + + expect(response.status).to eq(200) + expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(1) + expect(json["id"]).to be_present + expect(json["short_url"]).to eq("upload://qUm0DGR49PAZshIi7HxMd3cAlzn.png") + end + + it 'correctly sets retain_hours for admins' do + sign_in(Fabricate(:admin)) + + post "/uploads.json", params: { + file: logo, + retain_hours: 100, + type: "profile_background", + } + + id = JSON.parse(response.body)["id"] + expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0) + expect(Upload.find(id).retain_hours).to eq(100) + end + + it 'requires a file' do + post "/uploads.json", params: { type: "composer" } + + expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0) + message = JSON.parse(response.body) + expect(response.status).to eq 422 + expect(message["errors"]).to contain_exactly(I18n.t("upload.file_missing")) + end + + it 'properly returns errors' do + SiteSetting.authorized_extensions = "*" + SiteSetting.max_attachment_size_kb = 1 + + post "/uploads.json", params: { file: text_file, type: "avatar" } + + expect(response.status).to eq(422) + expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0) + errors = JSON.parse(response.body)["errors"] + expect(errors.first).to eq(I18n.t("upload.attachments.too_large", max_size_kb: 1)) + end + + it 'ensures allow_uploaded_avatars is enabled when uploading an avatar' do + SiteSetting.allow_uploaded_avatars = false + post "/uploads.json", params: { file: logo, type: "avatar" } + expect(response.status).to eq(422) + end + + it 'ensures sso_overrides_avatar is not enabled when uploading an avatar' do + SiteSetting.sso_overrides_avatar = true + post "/uploads.json", params: { file: logo, type: "avatar" } + expect(response.status).to eq(422) + end + + it 'allows staff to upload any file in PM' do + SiteSetting.authorized_extensions = "jpg" + SiteSetting.allow_staff_to_upload_any_file_in_pm = true + user.update_columns(moderator: true) + + post "/uploads.json", params: { + file: text_file, + type: "composer", + for_private_message: "true", + } + + expect(response.status).to eq(200) + id = JSON.parse(response.body)["id"] + expect(id).to be_present + end + + it 'respects `authorized_extensions_for_staff` setting when staff upload file' do + SiteSetting.authorized_extensions = "" + SiteSetting.authorized_extensions_for_staff = "*" + user.update_columns(moderator: true) + + post "/uploads.json", params: { + file: text_file, + type: "composer", + } + + expect(response.status).to eq(200) + data = JSON.parse(response.body) + expect(data["id"]).to be_present + end + + it 'ignores `authorized_extensions_for_staff` setting when non-staff upload file' do + SiteSetting.authorized_extensions = "" + SiteSetting.authorized_extensions_for_staff = "*" + + post "/uploads.json", params: { + file: text_file, + type: "composer", + } + + data = JSON.parse(response.body) + expect(data["errors"].first).to eq(I18n.t("upload.unauthorized", authorized_extensions: '')) + end + + it 'returns an error when it could not determine the dimensions of an image' do + post "/uploads.json", params: { file: fake_jpg, type: "composer" } + + expect(response.status).to eq(422) + expect(Jobs::CreateAvatarThumbnails.jobs.size).to eq(0) + message = JSON.parse(response.body)["errors"] + expect(message).to contain_exactly(I18n.t("upload.images.size_not_found")) + end + end + end + + describe '#show' do + let(:site) { "default" } + let(:sha) { Digest::SHA1.hexdigest("discourse") } + let(:user) { Fabricate(:user) } + + def upload_file(file) + fake_logo = Rack::Test::UploadedFile.new(file_from_fixtures(file)) + SiteSetting.authorized_extensions = "*" + sign_in(user) + + post "/uploads.json", params: { + file: fake_logo, + type: "composer", + } + url = JSON.parse(response.body)["url"] + upload = Upload.where(url: url).first + upload + end + + it "returns 404 when using external storage" do + SiteSetting.enable_s3_uploads = true + SiteSetting.s3_access_key_id = "fakeid7974664" + SiteSetting.s3_secret_access_key = "fakesecretid7974664" + + get "/uploads/#{site}/#{sha}.pdf" + expect(response.response_code).to eq(404) + end + + it "returns 404 when the upload doesn't exist" do + get "/uploads/#{site}/#{sha}.pdf" + expect(response.status).to eq(404) + end + + it 'uses send_file' do + upload = upload_file("logo.png") + get "/uploads/#{site}/#{upload.sha1}.#{upload.extension}" + expect(response.status).to eq(200) + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"logo.png\"") + end + + it "handles file without extension" do + SiteSetting.authorized_extensions = "*" + upload = upload_file("image_no_extension") + + get "/uploads/#{site}/#{upload.sha1}.json" + expect(response.status).to eq(200) + expect(response.headers["Content-Disposition"]).to eq("attachment; filename=\"image_no_extension\"") + end + + context "prevent anons from downloading files" do + it "returns 404 when an anonymous user tries to download a file" do + upload = upload_file("logo.png") + delete "/session/#{user.username}.json" # upload a file, then sign out + + SiteSetting.prevent_anons_from_downloading_files = true + get "/uploads/#{site}/#{upload.sha1}.#{upload.extension}" + expect(response.status).to eq(404) + end + end + end + + describe '#lookup_urls' do + it 'can look up long urls' do + sign_in(Fabricate(:user)) + upload = Fabricate(:upload) + + post "/uploads/lookup-urls.json", params: { short_urls: [upload.short_url] } + expect(response.status).to eq(200) + + result = JSON.parse(response.body) + expect(result[0]["url"]).to eq(upload.url) + end + end +end diff --git a/spec/controllers/user_actions_controller_spec.rb b/spec/requests/user_actions_controller_spec.rb similarity index 81% rename from spec/controllers/user_actions_controller_spec.rb rename to spec/requests/user_actions_controller_spec.rb index 79cf2ac108..3bae24ecfa 100644 --- a/spec/controllers/user_actions_controller_spec.rb +++ b/spec/requests/user_actions_controller_spec.rb @@ -5,16 +5,15 @@ describe UserActionsController do context 'index' do it 'fails if username is not specified' do - expect do - get :index, format: :json - end.to raise_error(ActionController::ParameterMissing) + get "/user_actions.json" + expect(response.status).to eq(400) end it 'renders list correctly' do UserActionCreator.enable post = Fabricate(:post) - get :index, params: { username: post.user.username }, format: :json + get "/user_actions.json", params: { username: post.user.username } expect(response.status).to eq(200) parsed = JSON.parse(response.body) @@ -27,29 +26,28 @@ describe UserActionsController do end it 'renders help text if provided for self' do - logged_in = log_in + logged_in = sign_in(Fabricate(:user)) - get :index, params: { + get "/user_actions.json", params: { filter: UserAction::LIKE, username: logged_in.username, no_results_help_key: "user_activity.no_bookmarks" - }, format: :json + } expect(response.status).to eq(200) parsed = JSON.parse(response.body) expect(parsed["no_results_help"]).to eq(I18n.t("user_activity.no_bookmarks.self")) - end it 'renders help text for others' do user = Fabricate(:user) - get :index, params: { + get "/user_actions.json", params: { filter: UserAction::LIKE, username: user.username, no_results_help_key: "user_activity.no_bookmarks" - }, format: :json + } expect(response.status).to eq(200) parsed = JSON.parse(response.body) @@ -61,23 +59,22 @@ describe UserActionsController do context "without access" do let(:user) { Fabricate(:user) } it "raises an exception" do - get :index, params: { + get "/user_actions.json", params: { username: user.username, filter: UserAction::PENDING - }, format: :json - expect(response).to_not be_success - + } + expect(response).to be_forbidden end end context "with access" do - let(:user) { log_in } + let(:user) { sign_in(Fabricate(:user)) } it 'finds queued posts' do queued_post = PostEnqueuer.new(user, 'default').enqueue(raw: 'this is the raw enqueued content') - get :index, params: { + get "/user_actions.json", params: { username: user.username, filter: UserAction::PENDING - }, format: :json + } expect(response.status).to eq(200) parsed = JSON.parse(response.body) diff --git a/spec/controllers/user_api_keys_controller_spec.rb b/spec/requests/user_api_keys_controller_spec.rb similarity index 81% rename from spec/controllers/user_api_keys_controller_spec.rb rename to spec/requests/user_api_keys_controller_spec.rb index 5e430cc5c0..34fc6c898b 100644 --- a/spec/controllers/user_api_keys_controller_spec.rb +++ b/spec/requests/user_api_keys_controller_spec.rb @@ -46,8 +46,8 @@ describe UserApiKeysController do context 'new' do it "supports a head request cleanly" do - head :new - expect(response.code).to eq("200") + head "/user-api-key/new" + expect(response.status).to eq(200) expect(response.headers["Auth-Api-Version"]).to eq("2") end end @@ -55,14 +55,14 @@ describe UserApiKeysController do context 'create' do it "does not allow anon" do - post :create, params: args, format: :json + post "/user-api-key.json", params: args expect(response.status).to eq(403) end it "refuses to redirect to disallowed place" do - log_in_user(Fabricate(:user)) - post :create, params: args, format: :json - expect(response.code).to eq("403") + sign_in(Fabricate(:user)) + post "/user-api-key.json", params: args + expect(response.status).to eq(403) end it "will allow tokens for staff without TL" do @@ -71,10 +71,10 @@ describe UserApiKeysController do user = Fabricate(:user, trust_level: 1, moderator: true) - log_in_user(user) + sign_in(user) - post :create, params: args, format: :json - expect(response.code).to eq("302") + post "/user-api-key.json", params: args + expect(response.status).to eq(302) end it "will not create token unless TL is met" do @@ -82,36 +82,29 @@ describe UserApiKeysController do SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect] user = Fabricate(:user, trust_level: 1) + sign_in(user) - log_in_user(user) - - post :create, params: args, format: :json - expect(response.code).to eq("403") - + post "/user-api-key.json", params: args + expect(response.status).to eq(403) end it "will deny access if requesting more rights than allowed" do - SiteSetting.min_trust_level_for_user_api_key = 0 SiteSetting.allowed_user_api_auth_redirects = args[:auth_redirect] SiteSetting.allow_user_api_key_scopes = "write" user = Fabricate(:user, trust_level: 0) + sign_in(user) - log_in_user(user) - - post :create, params: args, format: :json - expect(response.code).to eq("403") - + post "/user-api-key.json", params: args + expect(response.status).to eq(403) end it "allows for a revoke with no id" do key = Fabricate(:readonly_user_api_key) - request.env['HTTP_USER_API_KEY'] = key.key - post :revoke, format: :json + post "/user-api-key/revoke.json", headers: { HTTP_USER_API_KEY: key.key } expect(response.status).to eq(200) - key.reload expect(key.revoked_at).not_to eq(nil) end @@ -120,19 +113,20 @@ describe UserApiKeysController do key1 = Fabricate(:readonly_user_api_key) key2 = Fabricate(:readonly_user_api_key) - request.env['HTTP_USER_API_KEY'] = key1.key - post :revoke, params: { id: key2.id }, format: :json + post "/user-api-key/revoke.json", + params: { id: key2.id }, + headers: { HTTP_USER_API_KEY: key1.key } expect(response.status).to eq(403) end it "will allow readonly api keys to revoke self" do key = Fabricate(:readonly_user_api_key) - request.env['HTTP_USER_API_KEY'] = key.key - post :revoke, params: { id: key.id }, format: :json + post "/user-api-key/revoke.json", + params: { id: key.id }, + headers: { HTTP_USER_API_KEY: key.key } expect(response.status).to eq(200) - key.reload expect(key.revoked_at).not_to eq(nil) end @@ -145,11 +139,10 @@ describe UserApiKeysController do args[:push_url] = "https://push.it/here" user = Fabricate(:user, trust_level: 0) + sign_in(user) - log_in_user(user) - - post :create, params: args, format: :json - expect(response.code).to eq("302") + post "/user-api-key.json", params: args + expect(response.status).to eq(302) uri = URI.parse(response.redirect_url) @@ -168,7 +161,6 @@ describe UserApiKeysController do key = user.user_api_keys.first expect(key.scopes).to include("push") expect(key.push_url).to eq("https://push.it/here") - end it "will redirect correctly with valid token" do @@ -180,11 +172,10 @@ describe UserApiKeysController do args[:push_url] = "https://push.it/here" user = Fabricate(:user, trust_level: 0) + sign_in(user) - log_in_user(user) - - post :create, params: args, format: :json - expect(response.code).to eq("302") + post "/user-api-key.json", params: args + expect(response.status).to eq(302) uri = URI.parse(response.redirect_url) @@ -210,10 +201,9 @@ describe UserApiKeysController do # should overwrite if needed args["access"] = "pr" - post :create, params: args, format: :json + post "/user-api-key.json", params: args - expect(response.code).to eq("302") + expect(response.status).to eq(302) end - end end diff --git a/spec/controllers/user_avatars_controller_spec.rb b/spec/requests/user_avatars_controller_spec.rb similarity index 72% rename from spec/controllers/user_avatars_controller_spec.rb rename to spec/requests/user_avatars_controller_spec.rb index 7218cd8a06..be10954e8a 100644 --- a/spec/controllers/user_avatars_controller_spec.rb +++ b/spec/requests/user_avatars_controller_spec.rb @@ -5,19 +5,12 @@ describe UserAvatarsController do context 'show_proxy_letter' do it 'returns not found if external avatar is set somewhere else' do SiteSetting.external_system_avatars_url = "https://somewhere.else.com/avatar.png" - - get :show_proxy_letter, params: { - version: 'v2', letter: 'a', color: 'aaaaaa', size: 20 - }, format: :json - + get "/letter_avatar_proxy/v2/letter/a/aaaaaa/20.png" expect(response.status).to eq(404) end it 'returns an avatar if we are allowing the proxy' do - get :show_proxy_letter, params: { - version: 'v2', letter: 'a', color: 'aaaaaa', size: 360 - }, format: :json - + get "/letter_avatar_proxy/v2/letter/a/aaaaaa/360.png" expect(response.status).to eq(200) end end @@ -47,16 +40,12 @@ describe UserAvatarsController do user = Fabricate(:user, uploaded_avatar_id: upload.id) - get :show, params: { - size: 97, username: user.username, version: upload.id, hostname: 'default' - }, format: :json + get "/user_avatar/default/#{user.username}/97/#{upload.id}.png" # 98 is closest which is 49 * 2 for retina expect(response).to redirect_to("http://awesome.com/boom/user_avatar/default/#{user.username_lower}/98/#{upload.id}_#{OptimizedImage::VERSION}.png") - get :show, params: { - size: 98, username: user.username, version: upload.id, hostname: 'default' - }, format: :json + get "/user_avatar/default/#{user.username}/98/#{upload.id}.png" expect(response.body).to eq("image") expect(response.headers["Cache-Control"]).to eq('max-age=31556952, public, immutable') @@ -68,11 +57,9 @@ describe UserAvatarsController do upload = Fabricate(:upload) user = Fabricate(:user, uploaded_avatar_id: upload.id) - get :show, params: { - size: 51, username: user.username, version: upload.id, hostname: 'default' - }, format: :json + get "/user_avatar/default/#{user.username}/51/#{upload.id}.png" - expect(response).to be_success + expect(response.status).to eq(200) end end end diff --git a/spec/controllers/user_badges_controller_spec.rb b/spec/requests/user_badges_controller_spec.rb similarity index 65% rename from spec/controllers/user_badges_controller_spec.rb rename to spec/requests/user_badges_controller_spec.rb index 0d9dd609ef..f565caa1a7 100644 --- a/spec/controllers/user_badges_controller_spec.rb +++ b/spec/requests/user_badges_controller_spec.rb @@ -8,10 +8,10 @@ describe UserBadgesController do let(:badge) { Fabricate(:badge, target_posts: true, show_posts: false) } it 'does not leak private info' do p = create_post - UserBadge.create(badge: badge, user: user, post_id: p.id, granted_by_id: -1, granted_at: Time.now) + UserBadge.create!(badge: badge, user: user, post_id: p.id, granted_by_id: -1, granted_at: Time.now) - get :index, params: { badge_id: badge.id }, format: :json - expect(response).to be_success + get "/user_badges.json", params: { badge_id: badge.id } + expect(response.status).to eq(200) parsed = JSON.parse(response.body) expect(parsed["topics"]).to eq(nil) @@ -21,7 +21,7 @@ describe UserBadgesController do it "fails when badges are disabled" do SiteSetting.enable_badges = false - get :index, params: { badge_id: badge.id }, format: :json + get "/user_badges.json", params: { badge_id: badge.id } expect(response.status).to eq(404) end end @@ -30,13 +30,12 @@ describe UserBadgesController do let!(:user_badge) { UserBadge.create(badge: badge, user: user, granted_by: Discourse.system_user, granted_at: Time.now) } it 'requires username or badge_id to be specified' do - expect do - get :index, format: :json - end.to raise_error(ActionController::ParameterMissing) + get "/user_badges.json" + expect(response.status).to eq(400) end it 'returns user_badges for a user' do - get :username, params: { username: user.username }, format: :json + get "/user-badges/#{user.username}.json" expect(response.status).to eq(200) parsed = JSON.parse(response.body) @@ -44,7 +43,7 @@ describe UserBadgesController do end it 'returns user_badges for a badge' do - get :index, params: { badge_id: badge.id }, format: :json + get "/user_badges.json", params: { badge_id: badge.id } expect(response.status).to eq(200) parsed = JSON.parse(response.body) @@ -52,9 +51,9 @@ describe UserBadgesController do end it 'includes counts when passed the aggregate argument' do - get :username, params: { - username: user.username, grouped: true - }, format: :json + get "/user-badges/#{user.username}.json", params: { + grouped: true + } expect(response.status).to eq(200) parsed = JSON.parse(response.body) @@ -64,17 +63,16 @@ describe UserBadgesController do context 'create' do it 'requires username to be specified' do - expect do - post :create, params: { badge_id: badge.id }, format: :json - end.to raise_error(ActionController::ParameterMissing) + post "/user_badges.json", params: { badge_id: badge.id } + expect(response.status).to eq(400) end it 'does not allow regular users to grant badges' do - log_in_user Fabricate(:user) + sign_in(Fabricate(:user)) - post :create, params: { + post "/user_badges.json", params: { badge_id: badge.id, username: user.username - }, format: :json + } expect(response.status).to eq(403) end @@ -83,15 +81,13 @@ describe UserBadgesController do admin = Fabricate(:admin) post_1 = create_post - log_in_user admin + sign_in(admin) - StaffActionLogger.any_instance.expects(:log_badge_grant).once - - post :create, params: { + post "/user_badges.json", params: { badge_id: badge.id, username: user.username, reason: Discourse.base_url + post_1.url - }, format: :json + } expect(response.status).to eq(200) @@ -100,43 +96,43 @@ describe UserBadgesController do expect(user_badge).to be_present expect(user_badge.granted_by).to eq(admin) expect(user_badge.post_id).to eq(post_1.id) + expect(UserHistory.where(acting_user: admin, target_user: user).count).to eq(1) end it 'does not grant badges from regular api calls' do Fabricate(:api_key, user: user) - post :create, params: { + post "/user_badges.json", params: { badge_id: badge.id, username: user.username, api_key: user.api_key.key - }, format: :json + } expect(response.status).to eq(403) end it 'grants badges from master api calls' do api_key = Fabricate(:api_key) - StaffActionLogger.any_instance.expects(:log_badge_grant).never - post :create, params: { + post "/user_badges.json", params: { badge_id: badge.id, username: user.username, api_key: api_key.key, api_username: "system" - }, format: :json + } expect(response.status).to eq(200) user_badge = UserBadge.find_by(user: user, badge: badge) expect(user_badge).to be_present expect(user_badge.granted_by).to eq(Discourse.system_user) + expect(UserHistory.where(acting_user: Discourse.system_user, target_user: user).count).to eq(0) end it 'will trigger :user_badge_granted' do - log_in :admin - user + sign_in(Fabricate(:admin)) - event = DiscourseEvent.track_events do - post :create, params: { + events = DiscourseEvent.track_events do + post "/user_badges.json", params: { badge_id: badge.id, username: user.username - }, format: :json - end.first + } + end.map { |event| event[:event_name] } - expect(event[:event_name]).to eq(:user_badge_granted) + expect(events).to include(:user_badge_granted) end end @@ -144,26 +140,28 @@ describe UserBadgesController do let!(:user_badge) { UserBadge.create(badge: badge, user: user, granted_by: Discourse.system_user, granted_at: Time.now) } it 'checks that the user is authorized to revoke a badge' do - delete :destroy, params: { id: user_badge.id }, format: :json + delete "/user_badges/#{user_badge.id}.json" expect(response.status).to eq(403) end it 'revokes the badge' do - log_in :admin - StaffActionLogger.any_instance.expects(:log_badge_revoke).once - delete :destroy, params: { id: user_badge.id }, format: :json + admin = Fabricate(:admin) + sign_in(admin) + delete "/user_badges/#{user_badge.id}.json" + expect(response.status).to eq(200) expect(UserBadge.find_by(id: user_badge.id)).to eq(nil) + expect(UserHistory.where(acting_user: admin, target_user: user).count).to eq(1) end it 'will trigger :user_badge_removed' do - log_in :admin + sign_in(Fabricate(:admin)) - event = DiscourseEvent.track_events do - delete :destroy, params: { id: user_badge.id }, format: :json - end.first + events = DiscourseEvent.track_events do + delete "/user_badges/#{user_badge.id}.json" + end.map { |event| event[:event_name] } - expect(event[:event_name]).to eq(:user_badge_removed) + expect(events).to include(:user_badge_removed) end end end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 5b7e588c52..a99aacba9a 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -64,7 +64,7 @@ describe UsersController do :user_logged_in, :user_first_logged_in ) - expect(response).to be_success + expect(response.status).to eq(200) expect(flash[:error]).to be_blank expect(session[:current_user_id]).to be_present @@ -81,7 +81,7 @@ describe UsersController do end it 'should return the right response' do - expect(response).to be_success + expect(response.status).to eq(200) expect(CGI.unescapeHTML(response.body)) .to include(I18n.t('activation.approval_required')) @@ -116,7 +116,7 @@ describe UsersController do it "returns success" do SiteSetting.login_required = true get "/u/password-reset/#{token}" - expect(response).to be_success + expect(response.status).to eq(200) expect(CGI.unescapeHTML(response.body)).to include(I18n.t('password_reset.no_token')) end end @@ -127,7 +127,7 @@ describe UsersController do end it 'disallows login' do - expect(response).to be_success + expect(response.status).to eq(200) expect(CGI.unescapeHTML(response.body)) .to include(I18n.t('password_reset.no_token')) @@ -144,7 +144,7 @@ describe UsersController do it 'disallows login' do get "/u/password-reset/ev!l_trout@!" - expect(response).to be_success + expect(response.status).to eq(200) expect(CGI.unescapeHTML(response.body)) .to include(I18n.t('password_reset.no_token')) @@ -159,7 +159,7 @@ describe UsersController do it "responds with proper error message" do put "/u/password-reset/evil_trout!.json", params: { password: "awesomeSecretPassword" } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)["message"]).to eq(I18n.t('password_reset.no_token')) expect(session[:current_user_id]).to be_blank end @@ -189,7 +189,7 @@ describe UsersController do :user_logged_in, :user_first_logged_in ) - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include('{"is_developer":false,"admin":false,"second_factor_required":false}') expect(session["password-#{token}"]).to be_blank @@ -294,7 +294,7 @@ describe UsersController do it "fails when the password is blank" do put "/u/password-reset/#{token}.json", params: { password: '' } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)["errors"]).to be_present expect(session[:current_user_id]).to be_blank end @@ -302,7 +302,7 @@ describe UsersController do it "fails when the password is too long" do put "/u/password-reset/#{token}.json", params: { password: ('x' * (User.max_password_length + 1)) } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)["errors"]).to be_present expect(session[:current_user_id]).to be_blank end @@ -310,7 +310,7 @@ describe UsersController do it "logs in the user" do put "/u/password-reset/#{token}.json", params: { password: 'ksjafh928r' } - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)["errors"]).to be_blank expect(session[:current_user_id]).to be_present end @@ -331,14 +331,14 @@ describe UsersController do it "token doesn't match any records" do email_token = user.email_tokens.create(email: user.email) get "/u/confirm-email-token/#{SecureRandom.hex}.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(email_token.reload.confirmed).to eq(false) end it "token matches" do email_token = user.email_tokens.create(email: user.email) get "/u/confirm-email-token/#{email_token.token}.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(email_token.reload.confirmed).to eq(true) end end @@ -442,11 +442,11 @@ describe UsersController do user.save! post "/u/toggle-anon.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(AnonymousShadowCreator.get(user).id) post "/u/toggle-anon.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(user.id) end end @@ -934,7 +934,7 @@ describe UsersController do it "should succeed without the optional field" do post "/u.json", params: create_params - expect(response).to be_success + expect(response.status).to eq(200) inserted = User.find_by_email(@user.email) expect(inserted).to be_present expect(inserted.custom_fields).to be_present @@ -946,7 +946,7 @@ describe UsersController do it "should succeed with the optional field" do create_params[:user_fields][optional_field.id.to_s] = 'value3' post "/u.json", params: create_params.merge(create_params) - expect(response).to be_success + expect(response.status).to eq(200) inserted = User.find_by_email(@user.email) expect(inserted).to be_present expect(inserted.custom_fields).to be_present @@ -958,7 +958,7 @@ describe UsersController do it "trims excessively long fields" do create_params[:user_fields][optional_field.id.to_s] = ('x' * 3000) post "/u.json", params: create_params.merge(create_params) - expect(response).to be_success + expect(response.status).to eq(200) inserted = User.find_by_email(@user.email) val = inserted.custom_fields["user_field_#{optional_field.id}"] @@ -980,7 +980,7 @@ describe UsersController do it "should succeed" do post "/u.json", params: create_params - expect(response).to be_success + expect(response.status).to eq(200) inserted = User.find_by_email(@user.email) expect(inserted).to be_present expect(inserted.custom_fields).not_to be_present @@ -1073,7 +1073,7 @@ describe UsersController do it 'should succeed in normal circumstances' do put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.reload.username).to eq(new_username) end @@ -1097,7 +1097,7 @@ describe UsersController do put "/u/#{user.username}/preferences/username.json", params: { new_username: new_username } - expect(response).to be_success + expect(response.status).to eq(200) expect(UserHistory.where(action: UserHistory.actions[:change_username], target_user_id: user.id, acting_user_id: acting_user.id)).to be_present expect(user.reload.username).to eq(new_username) end @@ -1221,7 +1221,7 @@ describe UsersController do user = Fabricate(:user) get "/u/#{user.username}/invited.json", params: { username: user.username } - expect(response).to be_success + expect(response.status).to eq(200) end it 'filters by email' do @@ -1366,14 +1366,14 @@ describe UsersController do it "should be able to update a user" do put "/u/#{user.username}.json", params: { name: 'test.test' } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.reload.name).to eq('test.test') end it "should be able to update a user" do put "/u/#{user.username}.json", params: { name: 'testing123' } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.reload.name).to eq('testing123') end end @@ -1391,7 +1391,7 @@ describe UsersController do user_fields: { user_field.id.to_s => 'happy' } } - expect(response).to be_success + expect(response.status).to eq(200) user.reload @@ -1417,7 +1417,7 @@ describe UsersController do watched_tags: "#{tags[0].name},#{tags[1].name}" } - expect(response).to be_success + expect(response.status).to eq(200) user.reload @@ -1460,7 +1460,7 @@ describe UsersController do it "should update the user field" do put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy' } } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.user_fields[user_field.id.to_s]).to eq 'happy' end @@ -1480,13 +1480,13 @@ describe UsersController do it "should retain existing user fields" do put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy', optional_field.id.to_s => 'feet' } } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.user_fields[user_field.id.to_s]).to eq('happy') expect(user.user_fields[optional_field.id.to_s]).to eq('feet') put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'sad' } } - expect(response).to be_success + expect(response.status).to eq(200) user.reload @@ -1501,7 +1501,7 @@ describe UsersController do it "does not update the user field" do put "/u/#{user.username}.json", params: { name: 'Jim Tom', user_fields: { user_field.id.to_s => 'happy' } } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.user_fields[user_field.id.to_s]).to be_blank end end @@ -1748,7 +1748,7 @@ describe UsersController do it 'can successfully pick the system avatar' do put "/u/#{user.username}/preferences/avatar/pick.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(user.reload.uploaded_avatar_id).to eq(nil) end @@ -1757,7 +1757,7 @@ describe UsersController do upload_id: upload.id, type: "gravatar" } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.reload.uploaded_avatar_id).to eq(upload.id) expect(user.user_avatar.reload.gravatar_upload_id).to eq(upload.id) end @@ -1767,7 +1767,7 @@ describe UsersController do upload_id: upload.id, type: "custom" } - expect(response).to be_success + expect(response.status).to eq(200) expect(user.reload.uploaded_avatar_id).to eq(upload.id) expect(user.user_avatar.reload.custom_upload_id).to eq(upload.id) end @@ -1807,7 +1807,7 @@ describe UsersController do delete "/u/#{user.username}/preferences/user_image.json", params: { type: 'profile_background' } expect(user.reload.user_profile.profile_background).to eq("") - expect(response).to be_success + expect(response.status).to eq(200) end end end @@ -1843,7 +1843,7 @@ describe UsersController do it "deletes your account when you're allowed to" do UserDestroyer.any_instance.expects(:destroy).with(user, anything).returns(user) delete "/u/#{user.username}.json" - expect(response).to be_success + expect(response.status).to eq(200) end end end @@ -1894,7 +1894,7 @@ describe UsersController do get "/u/#{Fabricate(:user).username}/emails.json" - expect(response).to be_success + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json["email"]).to be_present expect(json["associated_accounts"]).to be_present @@ -1906,7 +1906,7 @@ describe UsersController do get "/u/#{inactive_user.username}/emails.json" - expect(response).to be_success + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json["email"]).to be_present expect(json["associated_accounts"]).to be_present @@ -2034,7 +2034,7 @@ describe UsersController do create_post(user: user) get "/u/#{user.username_lower}/summary.json" - expect(response).to be_success + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json["user_summary"]["topic_count"]).to eq(1) @@ -2045,12 +2045,12 @@ describe UsersController do describe '#confirm_admin' do it "fails without a valid token" do get "/u/confirm-admin/invalid-token.josn" - expect(response).not_to be_success + expect(response).not_to be_successful end it "fails with a missing token" do get "/u/confirm-admin/a0a0a0a0a0.josn" - expect(response).to_not be_success + expect(response).to_not be_successful end it "succeeds with a valid code as anonymous" do @@ -2058,7 +2058,7 @@ describe UsersController do ac = AdminConfirmation.new(user, Fabricate(:admin)) ac.create_confirmation get "/u/confirm-admin/#{ac.token}.josn" - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(user.admin?).to eq(false) @@ -2071,7 +2071,7 @@ describe UsersController do ac = AdminConfirmation.new(user, admin) ac.create_confirmation get "/u/confirm-admin/#{ac.token}.josn", params: { token: ac.token } - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(user.admin?).to eq(false) @@ -2084,7 +2084,7 @@ describe UsersController do ac = AdminConfirmation.new(user, Fabricate(:admin)) ac.create_confirmation get "/u/confirm-admin/#{ac.token}.josn" - expect(response).to_not be_success + expect(response).to_not be_successful user.reload expect(user.admin?).to eq(false) @@ -2096,7 +2096,7 @@ describe UsersController do ac = AdminConfirmation.new(user, Fabricate(:admin)) ac.create_confirmation post "/u/confirm-admin/#{ac.token}.josn" - expect(response).to be_success + expect(response.status).to eq(200) user.reload expect(user.admin?).to eq(true) @@ -2285,7 +2285,7 @@ describe UsersController do it "returns success" do get "/u/#{user.username}.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)["user"]["username"]).to eq(user.username) end @@ -2320,7 +2320,7 @@ describe UsersController do it 'returns success' do get "/u/#{user.username}.json" - expect(response).to be_success + expect(response.status).to eq(200) json = JSON.parse(response.body) expect(json["user"]["has_title_badges"]).to eq(false) @@ -2328,20 +2328,20 @@ describe UsersController do it "returns not found when the username doesn't exist" do get "/u/madeuppity.json" - expect(response).not_to be_success + expect(response).not_to be_successful end it 'returns not found when the user is inactive' do inactive = Fabricate(:user, active: false) get "/u/#{inactive.username}.json" - expect(response).not_to be_success + expect(response).not_to be_successful end it 'returns success when show_inactive_accounts is true and user is logged in' do SiteSetting.show_inactive_accounts = true inactive = Fabricate(:user, active: false) get "/u/#{inactive.username}.json" - expect(response).to be_success + expect(response.status).to eq(200) end it "raises an error on invalid access" do @@ -2374,13 +2374,13 @@ describe UsersController do it "returns fetch for a matching external_id" do get "/u/by-external/997.json" - expect(response).to be_success + expect(response.status).to eq(200) expect(JSON.parse(response.body)["user"]["username"]).to eq(user.username) end it "returns not found when external_id doesn't match" do get "/u/by-external/99.json" - expect(response).not_to be_success + expect(response).not_to be_successful end end @@ -2414,7 +2414,7 @@ describe UsersController do it "should be able to view a user" do get "/u/#{user.username}" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(user.username) end @@ -2426,7 +2426,7 @@ describe UsersController do it "should be able to view a user" do get "/u/#{user.username}" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include(user.username) end end @@ -2435,7 +2435,7 @@ describe UsersController do describe '#badges' do it "renders fine by default" do get "/u/#{user.username}/badges" - expect(response).to be_success + expect(response.status).to eq(200) end it "fails if badges are disabled" do @@ -2449,7 +2449,7 @@ describe UsersController do it "returns a message when no session is present" do get "/u/account-created" - expect(response).to be_success + expect(response.status).to eq(200) body = response.body @@ -2470,7 +2470,7 @@ describe UsersController do user = create_user get "/u/account-created" - expect(response).to be_success + expect(response.status).to eq(200) expect(response.body).to include( "{\"message\":\"#{I18n.t("login.activate_email", email: user.email).gsub!(" "705a8ccd2ce932be8e98c221fe701c1b4a0afcb8bbd57726de", - "timestamp" => Time.now.to_i, + post "/webhooks/mailgun.json", params: { + "token" => token, + "timestamp" => timestamp, "event" => "dropped", "recipient" => email, - "Message-Id" => "<12345@il.com>" - }, format: :json + "Message-Id" => "<12345@il.com>", + "signature" => signature + } - expect(response).to be_success + expect(response.status).to eq(200) email_log.reload expect(email_log.bounced).to eq(true) expect(email_log.user.user_stat.bounce_score).to eq(2) end - end context "sendgrid" do - it "works" do user = Fabricate(:user, email: email) email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) - post :sendgrid, params: { + post "/webhooks/sendgrid.json", params: { "_json" => [ { "email" => email, @@ -48,46 +49,42 @@ describe WebhooksController do "status" => "5.0.0" } ] - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) email_log.reload expect(email_log.bounced).to eq(true) expect(email_log.user.user_stat.bounce_score).to eq(2) end - end context "mailjet" do - it "works" do user = Fabricate(:user, email: email) email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) - post :mailjet, params: { + post "/webhooks/mailjet.json", params: { "event" => "bounce", "email" => email, "hard_bounce" => true, "CustomID" => message_id - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) email_log.reload expect(email_log.bounced).to eq(true) expect(email_log.user.user_stat.bounce_score).to eq(2) end - end context "mandrill" do - it "works" do user = Fabricate(:user, email: email) email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) - post :mandrill, params: { + post "/webhooks/mandrill.json", params: { mandrill_events: [{ "event" => "hard_bounce", "msg" => { @@ -97,24 +94,22 @@ describe WebhooksController do } } }] - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) email_log.reload expect(email_log.bounced).to eq(true) expect(email_log.user.user_stat.bounce_score).to eq(2) end - end context "sparkpost" do - it "works" do user = Fabricate(:user, email: email) email_log = Fabricate(:email_log, user: user, message_id: message_id, to_address: email) - post :sparkpost, params: { + post "/webhooks/sparkpost.json", params: { "_json" => [{ "msys" => { "message_event" => { @@ -126,15 +121,13 @@ describe WebhooksController do } } }] - }, format: :json + } - expect(response).to be_success + expect(response.status).to eq(200) email_log.reload expect(email_log.bounced).to eq(true) expect(email_log.user.user_stat.bounce_score).to eq(2) end - end - end diff --git a/spec/controllers/wizard_controller_spec.rb b/spec/requests/wizard_controller_spec.rb similarity index 70% rename from spec/controllers/wizard_controller_spec.rb rename to spec/requests/wizard_controller_spec.rb index 0875e31c26..9c8c72f6e2 100644 --- a/spec/controllers/wizard_controller_spec.rb +++ b/spec/requests/wizard_controller_spec.rb @@ -1,51 +1,47 @@ require 'rails_helper' describe WizardController do - context 'wizard enabled' do - render_views - before do SiteSetting.wizard_enabled = true end it 'needs you to be logged in' do - get :index, format: :json + get "/wizard.json" expect(response.status).to eq(403) end it 'needs you to be logged in' do - get :index + get "/wizard" # for whatever reason, no access is 404 # we may want to revisit this at some point and make it 403 expect(response.status).to eq(404) end it "raises an error if you aren't an admin" do - log_in(:moderator) - get :index, format: :json + sign_in(Fabricate(:moderator)) + get "/wizard.json" expect(response).to be_forbidden end it "raises an error if the wizard is disabled" do SiteSetting.wizard_enabled = false - log_in(:admin) - get :index, format: :json + sign_in(Fabricate(:admin)) + get "/wizard.json" expect(response).to be_forbidden end it "renders the wizard if you are an admin" do - log_in(:admin) - get :index, format: :json - expect(response).to be_success + sign_in(Fabricate(:admin)) + get "/wizard.json" + expect(response.status).to eq(200) end it "returns JSON when the mime type is appropriate" do - log_in(:admin) - get :index, format: 'json' - expect(response).to be_success + sign_in(Fabricate(:admin)) + get "/wizard.json" + expect(response.status).to eq(200) expect(::JSON.parse(response.body).has_key?('wizard')).to eq(true) end end - end diff --git a/spec/serializers/post_serializer_spec.rb b/spec/serializers/post_serializer_spec.rb index 5e75727cfc..40d2574d70 100644 --- a/spec/serializers/post_serializer_spec.rb +++ b/spec/serializers/post_serializer_spec.rb @@ -80,7 +80,7 @@ describe PostSerializer do end context "a hidden post with add_raw enabled" do - let(:user) { Fabricate.build(:user) } + let(:user) { Fabricate.build(:user, id: 101) } let(:raw) { "Raw contents of the post." } def serialized_post_for_user(u) @@ -132,12 +132,19 @@ describe PostSerializer do end context "a hidden wiki post" do - let(:post) { Fabricate.build(:post, raw: raw, user: user, wiki: true, hidden: true, hidden_reason_id: Post.hidden_reasons[:flag_threshold_reached]) } + let(:post) { + Fabricate.build( + :post, + raw: raw, + user: user, + wiki: true, + hidden: true, + hidden_reason_id: Post.hidden_reasons[:flag_threshold_reached]) + } it "can view edit history only if authorized" do expect(serialized_post_for_user(nil)[:can_view_edit_history]).to eq(false) expect(serialized_post_for_user(Fabricate(:user))[:can_view_edit_history]).to eq(false) - expect(serialized_post_for_user(user)[:can_view_edit_history]).to eq(true) expect(serialized_post_for_user(Fabricate(:moderator))[:can_view_edit_history]).to eq(true) expect(serialized_post_for_user(Fabricate(:admin))[:can_view_edit_history]).to eq(true) diff --git a/spec/serializers/single_sign_on_record_serializer_spec.rb b/spec/serializers/single_sign_on_record_serializer_spec.rb new file mode 100644 index 0000000000..6564f343f3 --- /dev/null +++ b/spec/serializers/single_sign_on_record_serializer_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +RSpec.describe SingleSignOnRecordSerializer do + let(:user) { Fabricate(:user) } + let :sso do + SingleSignOnRecord.create!(user_id: user.id, external_id: '12345', external_email: user.email, last_payload: '') + end + + context "admin" do + let(:admin) { Fabricate(:admin) } + let :serializer do + SingleSignOnRecordSerializer.new(sso, scope: Guardian.new(admin), root: false) + end + + it "should include user sso info" do + payload = serializer.as_json + expect(payload[:user_id]).to eq(user.id) + expect(payload[:external_id]).to eq('12345') + expect(payload[:external_email]).to eq(user.email) + end + end + + context "moderator" do + let(:moderator) { Fabricate(:moderator) } + let :serializer do + SingleSignOnRecordSerializer.new(sso, scope: Guardian.new(moderator), root: false) + end + + it "should include user sso info" do + payload = serializer.as_json + expect(payload[:user_id]).to eq(user.id) + expect(payload[:external_id]).to eq('12345') + expect(payload[:external_email]).to be_nil + end + end +end diff --git a/spec/services/badge_granter_spec.rb b/spec/services/badge_granter_spec.rb index 64d4b96fa7..451cb158a1 100644 --- a/spec/services/badge_granter_spec.rb +++ b/spec/services/badge_granter_spec.rb @@ -118,7 +118,8 @@ describe BadgeGranter do describe 'grant' do it 'allows overriding of granted_at does not notify old bronze' do - badge = Fabricate(:badge, badge_type_id: BadgeType::Bronze) + badge = Badge.create!(name: 'a badge', badge_type_id: BadgeType::Bronze) + time = 1.year.ago user_badge = BadgeGranter.grant(badge, user, created_at: time) diff --git a/spec/services/destroy_task_spec.rb b/spec/services/destroy_task_spec.rb index 1319393853..afcb205250 100644 --- a/spec/services/destroy_task_spec.rb +++ b/spec/services/destroy_task_spec.rb @@ -4,11 +4,11 @@ describe DestroyTask do describe 'destroy topics' do let!(:c) { Fabricate(:category) } - let!(:t) { Fabricate(:topic, category_id: c.id) } - let!(:p) { Fabricate(:post, topic_id: t.id) } + let!(:t) { Fabricate(:topic, category: c) } + let!(:p) { Fabricate(:post, topic: t) } let!(:c2) { Fabricate(:category) } - let!(:t2) { Fabricate(:topic, category_id: c2.id) } - let!(:p2) { Fabricate(:post, topic_id: t2.id) } + let!(:t2) { Fabricate(:topic, category: c2) } + let!(:p2) { Fabricate(:post, topic: t2) } it 'destroys all topics in a category' do before_count = Topic.where(category_id: c.id).count diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index 87408d112f..cf782d251a 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -36,13 +36,14 @@ describe PostAlerter do context "private message" do it "notifies for pms correctly" do pm = Fabricate(:topic, archetype: 'private_message', category_id: nil) - op = Fabricate(:post, user_id: pm.user_id) + op = Fabricate(:post, user: pm.user) pm.allowed_users << pm.user PostAlerter.post_created(op) - reply = Fabricate(:post, user_id: pm.user_id, topic_id: pm.id, reply_to_post_number: 1) + + reply = Fabricate(:post, user: pm.user, topic: pm, reply_to_post_number: 1) PostAlerter.post_created(reply) - reply2 = Fabricate(:post, topic_id: pm.id, reply_to_post_number: 1) + reply2 = Fabricate(:post, topic: pm, reply_to_post_number: 1) PostAlerter.post_created(reply2) # we get a green notification for a reply @@ -52,7 +53,7 @@ describe PostAlerter do Notification.destroy_all - reply3 = Fabricate(:post, topic_id: pm.id) + reply3 = Fabricate(:post, topic: pm) PostAlerter.post_created(reply3) # no notification cause we are tracking @@ -60,7 +61,7 @@ describe PostAlerter do Notification.destroy_all - reply4 = Fabricate(:post, topic_id: pm.id, reply_to_post_number: 1) + reply4 = Fabricate(:post, topic: pm, reply_to_post_number: 1) PostAlerter.post_created(reply4) # yes notification cause we were replied to @@ -70,7 +71,7 @@ describe PostAlerter do it "triggers :before_create_notifications_for_users" do pm = Fabricate(:topic, archetype: 'private_message', category_id: nil) - op = Fabricate(:post, user_id: pm.user_id, topic: pm) + op = Fabricate(:post, user: pm.user, topic: pm) user1 = Fabricate(:user) user2 = Fabricate(:user) group = Fabricate(:group, users: [user2]) diff --git a/spec/services/post_owner_changer_spec.rb b/spec/services/post_owner_changer_spec.rb index b9f836533e..aeeaeea605 100644 --- a/spec/services/post_owner_changer_spec.rb +++ b/spec/services/post_owner_changer_spec.rb @@ -5,19 +5,19 @@ describe PostOwnerChanger do let!(:editor) { Fabricate(:admin) } let(:topic) { Fabricate(:topic) } let(:user_a) { Fabricate(:user) } - let(:p1) { Fabricate(:post, topic_id: topic.id, post_number: 1) } - let(:p2) { Fabricate(:post, topic_id: topic.id, post_number: 2) } + let(:p1) { Fabricate(:post, topic: topic, post_number: 1) } + let(:p2) { Fabricate(:post, topic: topic, post_number: 2) } let(:p3) { Fabricate(:post) } it "raises an error with a parameter missing" do expect { - described_class.new(post_ids: [p1.id], topic_id: topic.id, new_owner: nil, acting_user: editor) + PostOwnerChanger.new(post_ids: [p1.id], topic_id: topic.id, new_owner: nil, acting_user: editor) }.to raise_error(ArgumentError) end it "calls PostRevisor" do PostRevisor.any_instance.expects(:revise!) - described_class.new(post_ids: [p1.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! + PostOwnerChanger.new(post_ids: [p1.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! end it "changes the user" do @@ -39,7 +39,7 @@ describe PostOwnerChanger do end it "changes multiple posts" do - described_class.new(post_ids: [p1.id, p2.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! + PostOwnerChanger.new(post_ids: [p1.id, p2.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! p1.reload; p2.reload expect(p1.user).not_to eq(nil) expect(p1.user).to eq(user_a) @@ -47,7 +47,7 @@ describe PostOwnerChanger do end it "ignores posts in other topics" do - described_class.new(post_ids: [p1.id, p3.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! + PostOwnerChanger.new(post_ids: [p1.id, p3.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! p1.reload; p3.reload expect(p1.user).to eq(user_a) @@ -56,7 +56,7 @@ describe PostOwnerChanger do end it "skips creating new post revision if skip_revision is true" do - described_class.new(post_ids: [p1.id, p2.id], topic_id: topic.id, new_owner: user_a, acting_user: editor, skip_revision: true).change_owner! + PostOwnerChanger.new(post_ids: [p1.id, p2.id], topic_id: topic.id, new_owner: user_a, acting_user: editor, skip_revision: true).change_owner! p1.reload; p2.reload expect(p1.revisions.size).to eq(0) expect(p2.revisions.size).to eq(0) @@ -76,7 +76,7 @@ describe PostOwnerChanger do end it "changes the owner when the post is deleted" do - p4 = Fabricate(:post, topic_id: topic.id, reply_to_post_number: p2.post_number) + p4 = Fabricate(:post, topic: topic, reply_to_post_number: p2.post_number) PostDestroyer.new(editor, p4).destroy PostOwnerChanger.new(post_ids: [p4.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! @@ -84,16 +84,16 @@ describe PostOwnerChanger do end context "sets topic notification level for the new owner" do - let(:p4) { Fabricate(:post, post_number: 2, topic_id: topic.id) } + let(:p4) { Fabricate(:post, post_number: 2, topic: topic) } it "'watching' if the first post gets a new owner" do - described_class.new(post_ids: [p1.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! + PostOwnerChanger.new(post_ids: [p1.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! tu = TopicUser.find_by(user_id: user_a.id, topic_id: topic.id) expect(tu.notification_level).to eq(3) end it "'tracking' if other than the first post gets a new owner" do - described_class.new(post_ids: [p4.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! + PostOwnerChanger.new(post_ids: [p4.id], topic_id: topic.id, new_owner: user_a, acting_user: editor).change_owner! tu = TopicUser.find_by(user_id: user_a.id, topic_id: topic.id) expect(tu.notification_level).to eq(2) end @@ -129,7 +129,7 @@ describe PostOwnerChanger do end subject(:change_owners) do - described_class.new( + PostOwnerChanger.new( post_ids: [p1.id, p2.id], topic_id: topic.id, new_owner: user_a, @@ -180,7 +180,7 @@ describe PostOwnerChanger do user_stat = editor.user_stat expect { - described_class.new( + PostOwnerChanger.new( post_ids: [whisper.id], topic_id: topic.id, new_owner: Fabricate(:admin), diff --git a/spec/services/staff_action_logger_spec.rb b/spec/services/staff_action_logger_spec.rb index 70132a7a51..56c8a81c36 100644 --- a/spec/services/staff_action_logger_spec.rb +++ b/spec/services/staff_action_logger_spec.rb @@ -488,4 +488,25 @@ describe StaffActionLogger do expect { log_post_approved }.to change { UserHistory.count }.by(1) end end + + describe 'log_post_rejected' do + let(:rejected_post) { Fabricate(:queued_post) } + + subject(:log_post_rejected) { described_class.new(admin).log_post_rejected(rejected_post) } + + it 'raises an error when post is nil' do + expect { logger.log_post_rejected(nil) }.to raise_error(Discourse::InvalidParameters) + end + + it 'raises an error when post is not a QueuedPosts' do + expect { logger.log_post_rejected(1) }.to raise_error(Discourse::InvalidParameters) + end + + it 'creates a new UserHistory record' do + expect { log_post_rejected }.to change { UserHistory.count }.by(1) + user_history = UserHistory.last + expect(user_history.action).to eq(UserHistory.actions[:post_rejected]) + expect(user_history.details).to include(rejected_post.raw) + end + end end diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb index f397bc930b..9212958d23 100644 --- a/spec/services/user_anonymizer_spec.rb +++ b/spec/services/user_anonymizer_spec.rb @@ -23,7 +23,9 @@ describe UserAnonymizer do end describe "make_anonymous" do - let(:user) { Fabricate(:user, username: "edward") } + let(:original_email) { "edward@example.net" } + let(:user) { Fabricate(:user, username: "edward", email: original_email) } + let(:another_user) { Fabricate(:evil_trout) } subject(:make_anonymous) { described_class.make_anonymous(user, admin) } it "changes username" do @@ -33,7 +35,7 @@ describe UserAnonymizer do it "changes email address" do make_anonymous - expect(user.reload.email).to eq("#{user.username}@example.com") + expect(user.reload.email).to eq("#{user.username}@anonymized.invalid") end it "turns off all notifications" do @@ -201,6 +203,60 @@ describe UserAnonymizer do expect(user.api_key).to eq(nil) end + context "executes job" do + before do + SiteSetting.queue_jobs = false + end + + it "removes invites" do + Fabricate(:invite, user: user) + Fabricate(:invite, user: another_user) + + expect { make_anonymous }.to change { Invite.count }.by(-1) + expect(Invite.where(user_id: user.id).count).to eq(0) + end + + it "removes email tokens" do + Fabricate(:email_token, user: user) + Fabricate(:email_token, user: another_user) + + expect { make_anonymous }.to change { EmailToken.count }.by(-1) + expect(EmailToken.where(user_id: user.id).count).to eq(0) + end + + it "removes email log entries" do + Fabricate(:email_log, user: user) + Fabricate(:email_log, user: another_user) + + expect { make_anonymous }.to change { EmailLog.count }.by(-1) + expect(EmailLog.where(user_id: user.id).count).to eq(0) + end + + it "removes incoming emails" do + Fabricate(:incoming_email, user: user, from_address: user.email) + Fabricate(:incoming_email, from_address: user.email, error: "Some error") + Fabricate(:incoming_email, user: another_user, from_address: another_user.email) + + expect { make_anonymous }.to change { IncomingEmail.count }.by(-2) + expect(IncomingEmail.where(user_id: user.id).count).to eq(0) + expect(IncomingEmail.where(from_address: original_email).count).to eq(0) + end + + it "removes raw email from posts" do + post1 = Fabricate(:post, user: user, via_email: true, raw_email: "raw email from user") + post2 = Fabricate(:post, user: another_user, via_email: true, raw_email: "raw email from another user") + + make_anonymous + + expect(post1.reload).to have_attributes(via_email: true, raw_email: nil) + expect(post2.reload).to have_attributes(via_email: true, raw_email: "raw email from another user") + end + + it "does not delete profile views" do + UserProfileView.add(user.id, '127.0.0.1', another_user.id, Time.now, true) + expect { make_anonymous }.to_not change { UserProfileView.count } + end + end end describe "anonymize_ip" do @@ -222,6 +278,7 @@ describe UserAnonymizer do end it "exhaustively replaces all user ips" do + SiteSetting.queue_jobs = false link = IncomingLink.create!(current_user_id: user.id, ip_address: old_ip, post_id: post.id) screened_email = ScreenedEmail.create!(email: user.email, ip_address: old_ip) diff --git a/spec/services/user_destroyer_spec.rb b/spec/services/user_destroyer_spec.rb index e958136b9d..0b506f5c7c 100644 --- a/spec/services/user_destroyer_spec.rb +++ b/spec/services/user_destroyer_spec.rb @@ -71,10 +71,15 @@ describe UserDestroyer do end context 'user deletes self' do - let(:destroy_opts) { { delete_posts: true } } + let(:destroy_opts) { { delete_posts: true, context: "/u/username/preferences/account" } } subject(:destroy) { UserDestroyer.new(@user).destroy(@user, destroy_opts) } include_examples "successfully destroy a user" + + it 'should log proper context' do + destroy + expect(UserHistory.where(action: UserHistory.actions[:delete_user]).last.context).to eq(I18n.t("staff_action_logs.user_delete_self", url: "/u/username/preferences/account")) + end end context "with a queued post" do diff --git a/spec/services/user_merger_spec.rb b/spec/services/user_merger_spec.rb index 2d81ad8e3b..fac984e6fc 100644 --- a/spec/services/user_merger_spec.rb +++ b/spec/services/user_merger_spec.rb @@ -850,6 +850,7 @@ describe UserMerger do UserHistory.create(action: UserHistory.actions[:anonymize_user], target_user_id: walter.id, acting_user_id: source_user.id) merge_users! + UserHistory.where(action: UserHistory.actions[:merge_user], target_user_id: target_user.id).delete_all expect(UserHistory.where(target_user_id: target_user.id).count).to eq(1) expect(UserHistory.where(target_user_id: source_user.id).count).to eq(0) @@ -953,6 +954,15 @@ describe UserMerger do expect(User.find_by_username(source_user.username)).to be_nil end + it "deletes the source user even when it is a member of a group that grants a trust level" do + group = Fabricate(:group, grant_trust_level: 3) + group.bulk_add([source_user.id, target_user.id]) + + merge_users! + + expect(User.find_by_username(source_user.username)).to be_nil + end + it "deletes external auth infos of source user" do FacebookUserInfo.create(user_id: source_user.id, facebook_user_id: "example") GithubUserInfo.create(user_id: source_user.id, screen_name: "example", github_user_id: "examplel123123") @@ -1019,4 +1029,15 @@ describe UserMerger do merge_users! end + + it "correctly logs the merge" do + expect { merge_users! }.to change { UserHistory.count }.by(1) + + log_entry = UserHistory.last + expect(log_entry.action).to eq(UserHistory.actions[:merge_user]) + expect(log_entry.acting_user_id).to eq(Discourse::SYSTEM_USER_ID) + expect(log_entry.target_user_id).to eq(target_user.id) + expect(log_entry.context).to eq(I18n.t("staff_action_logs.user_merged", username: source_user.username)) + expect(log_entry.email).to eq("alice@work.com") + end end diff --git a/spec/support/integration_helpers.rb b/spec/support/integration_helpers.rb index 94d245caae..f2d72771c6 100644 --- a/spec/support/integration_helpers.rb +++ b/spec/support/integration_helpers.rb @@ -2,7 +2,7 @@ module IntegrationHelpers def create_user get "/u/hp.json" - expect(response).to be_success + expect(response.status).to eq(200) body = JSON.parse(response.body) honeypot = body["value"] @@ -17,7 +17,7 @@ module IntegrationHelpers challenge: challenge.reverse } - expect(response).to be_success + expect(response.status).to eq(200) body = JSON.parse(response.body) User.find(body["user_id"]) diff --git a/test/javascripts/acceptance/about-test.js.es6 b/test/javascripts/acceptance/about-test.js.es6 index c7ef9fb85a..9393c068a3 100644 --- a/test/javascripts/acceptance/about-test.js.es6 +++ b/test/javascripts/acceptance/about-test.js.es6 @@ -4,9 +4,9 @@ acceptance("About"); QUnit.test("viewing", assert => { visit("/about"); andThen(() => { - assert.ok($('body.about-page').length, "has body class"); - assert.ok(exists('.about.admins .user-info'), 'has admins'); - assert.ok(exists('.about.moderators .user-info'), 'has moderators'); - assert.ok(exists('.about.stats tr td'), 'has stats'); + assert.ok($("body.about-page").length, "has body class"); + assert.ok(exists(".about.admins .user-info"), "has admins"); + assert.ok(exists(".about.moderators .user-info"), "has moderators"); + assert.ok(exists(".about.stats tr td"), "has stats"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/account-created-test.js.es6 b/test/javascripts/acceptance/account-created-test.js.es6 index a0fc11267a..a009024bb4 100644 --- a/test/javascripts/acceptance/account-created-test.js.es6 +++ b/test/javascripts/acceptance/account-created-test.js.es6 @@ -1,97 +1,99 @@ import { acceptance } from "helpers/qunit-helpers"; -import PreloadStore from 'preload-store'; +import PreloadStore from "preload-store"; acceptance("Account Created"); QUnit.test("account created - message", assert => { - PreloadStore.store('accountCreated', { - message: "Hello World", + PreloadStore.store("accountCreated", { + message: "Hello World" }); visit("/u/account-created"); andThen(() => { - assert.ok(exists('.account-created')); + assert.ok(exists(".account-created")); assert.equal( - find('.account-created .ac-message').text().trim(), + find(".account-created .ac-message") + .text() + .trim(), "Hello World", "it displays the message" ); - assert.notOk(exists('.activation-controls')); + assert.notOk(exists(".activation-controls")); }); }); QUnit.test("account created - resend email", assert => { - PreloadStore.store('accountCreated', { + PreloadStore.store("accountCreated", { message: "Hello World", - username: 'eviltrout', - email: 'eviltrout@example.com', + username: "eviltrout", + email: "eviltrout@example.com", show_controls: true }); visit("/u/account-created"); andThen(() => { - assert.ok(exists('.account-created')); + assert.ok(exists(".account-created")); assert.equal( - find('.account-created .ac-message').text().trim(), + find(".account-created .ac-message") + .text() + .trim(), "Hello World", "it displays the message" ); }); - click('.activation-controls .resend'); + click(".activation-controls .resend"); andThen(() => { assert.equal(currentPath(), "account-created.resent"); - const email = find('.account-created .ac-message b').text(); - assert.equal(email, 'eviltrout@example.com'); + const email = find(".account-created .ac-message b").text(); + assert.equal(email, "eviltrout@example.com"); }); - }); QUnit.test("account created - update email - cancel", assert => { - PreloadStore.store('accountCreated', { + PreloadStore.store("accountCreated", { message: "Hello World", - username: 'eviltrout', - email: 'eviltrout@example.com', + username: "eviltrout", + email: "eviltrout@example.com", show_controls: true }); visit("/u/account-created"); - click('.activation-controls .edit-email'); + click(".activation-controls .edit-email"); andThen(() => { assert.equal(currentPath(), "account-created.edit-email"); - assert.ok(find('.activation-controls .btn-primary:disabled').length); + assert.ok(find(".activation-controls .btn-primary:disabled").length); }); - click('.activation-controls .edit-cancel'); + click(".activation-controls .edit-cancel"); andThen(() => { assert.equal(currentPath(), "account-created.index"); }); }); QUnit.test("account created - update email - submit", assert => { - PreloadStore.store('accountCreated', { + PreloadStore.store("accountCreated", { message: "Hello World", - username: 'eviltrout', - email: 'eviltrout@example.com', + username: "eviltrout", + email: "eviltrout@example.com", show_controls: true }); visit("/u/account-created"); - click('.activation-controls .edit-email'); + click(".activation-controls .edit-email"); andThen(() => { - assert.ok(find('.activation-controls .btn-primary:disabled').length); + assert.ok(find(".activation-controls .btn-primary:disabled").length); }); - fillIn('.activate-new-email', 'newemail@example.com'); + fillIn(".activate-new-email", "newemail@example.com"); andThen(() => { - assert.notOk(find('.activation-controls .btn-primary:disabled').length); + assert.notOk(find(".activation-controls .btn-primary:disabled").length); }); - click('.activation-controls .btn-primary'); + click(".activation-controls .btn-primary"); andThen(() => { assert.equal(currentPath(), "account-created.resent"); - const email = find('.account-created .ac-message b').text(); - assert.equal(email, 'newemail@example.com'); + const email = find(".account-created .ac-message b").text(); + assert.equal(email, "newemail@example.com"); }); - -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index 3f38df94b1..9dcb5bf488 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -4,116 +4,136 @@ acceptance("Admin - Flagging", { loggedIn: true }); QUnit.test("flagged posts", assert => { visit("/admin/flags/active"); andThen(() => { - assert.equal(find('.flagged-posts .flagged-post').length, 1); - assert.equal(find('.flagged-post .flag-user').length, 1, 'shows who flagged it'); - assert.equal(find('.flagged-post-response').length, 2); - assert.equal(find('.flagged-post-response:eq(0) img.avatar').length, 1); - assert.equal(find('.flagged-post-user-details .username').length, 1, 'shows the flagged username'); + assert.equal(find(".flagged-posts .flagged-post").length, 1); + assert.equal( + find(".flagged-post .flag-user").length, + 1, + "shows who flagged it" + ); + assert.equal(find(".flagged-post-response").length, 2); + assert.equal(find(".flagged-post-response:eq(0) img.avatar").length, 1); + assert.equal( + find(".flagged-post-user-details .username").length, + 1, + "shows the flagged username" + ); }); }); QUnit.test("flagged posts - agree", assert => { - const agreeFlag = selectKit('.agree-flag'); + const agreeFlag = selectKit(".agree-flag"); visit("/admin/flags/active"); - agreeFlag.expand().selectRowByValue('confirm-agree-keep'); + agreeFlag.expand().selectRowByValue("confirm-agree-keep"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); + assert.equal( + find(".admin-flags .flagged-post").length, + 0, + "post was removed" + ); }); }); QUnit.test("flagged posts - agree + hide", assert => { - const agreeFlag = selectKit('.agree-flag'); + const agreeFlag = selectKit(".agree-flag"); visit("/admin/flags/active"); - agreeFlag.expand().selectRowByValue('confirm-agree-hide'); + agreeFlag.expand().selectRowByValue("confirm-agree-hide"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); + assert.equal( + find(".admin-flags .flagged-post").length, + 0, + "post was removed" + ); }); }); QUnit.test("flagged posts - agree + deleteSpammer", assert => { - const agreeFlag = selectKit('.agree-flag'); + const agreeFlag = selectKit(".agree-flag"); visit("/admin/flags/active"); - agreeFlag.expand().selectRowByValue('delete-spammer'); + agreeFlag.expand().selectRowByValue("delete-spammer"); - click('.confirm-delete'); + click(".confirm-delete"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); + assert.equal( + find(".admin-flags .flagged-post").length, + 0, + "post was removed" + ); }); }); QUnit.test("flagged posts - disagree", assert => { visit("/admin/flags/active"); - click('.disagree-flag'); + click(".disagree-flag"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0); + assert.equal(find(".admin-flags .flagged-post").length, 0); }); }); QUnit.test("flagged posts - defer", assert => { visit("/admin/flags/active"); - click('.defer-flag'); + click(".defer-flag"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0); + assert.equal(find(".admin-flags .flagged-post").length, 0); }); }); QUnit.test("flagged posts - delete + defer", assert => { - const deleteFlag = selectKit('.delete-flag'); + const deleteFlag = selectKit(".delete-flag"); visit("/admin/flags/active"); - deleteFlag.expand().selectRowByValue('delete-defer'); + deleteFlag.expand().selectRowByValue("delete-defer"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0); + assert.equal(find(".admin-flags .flagged-post").length, 0); }); }); QUnit.test("flagged posts - delete + agree", assert => { - const deleteFlag = selectKit('.delete-flag'); + const deleteFlag = selectKit(".delete-flag"); visit("/admin/flags/active"); - deleteFlag.expand().selectRowByValue('delete-agree'); + deleteFlag.expand().selectRowByValue("delete-agree"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0); + assert.equal(find(".admin-flags .flagged-post").length, 0); }); }); QUnit.test("flagged posts - delete + deleteSpammer", assert => { - const deleteFlag = selectKit('.delete-flag'); + const deleteFlag = selectKit(".delete-flag"); visit("/admin/flags/active"); - deleteFlag.expand().selectRowByValue('delete-spammer'); + deleteFlag.expand().selectRowByValue("delete-spammer"); - click('.confirm-delete'); + click(".confirm-delete"); andThen(() => { - assert.equal(find('.admin-flags .flagged-post').length, 0); + assert.equal(find(".admin-flags .flagged-post").length, 0); }); }); QUnit.test("topics with flags", assert => { visit("/admin/flags/topics"); andThen(() => { - assert.equal(find('.flagged-topics .flagged-topic').length, 1); - assert.equal(find('.flagged-topic .flagged-topic-user').length, 2); - assert.equal(find('.flagged-topic .flag-counts').length, 3); + assert.equal(find(".flagged-topics .flagged-topic").length, 1); + assert.equal(find(".flagged-topic .flagged-topic-user").length, 2); + assert.equal(find(".flagged-topic .flag-counts").length, 3); }); - click('.flagged-topic .show-details'); + click(".flagged-topic .show-details"); andThen(() => { - assert.equal(currentURL(), '/admin/flags/topics/280'); + assert.equal(currentURL(), "/admin/flags/topics/280"); }); }); diff --git a/test/javascripts/acceptance/admin-search-log-term-test.js.es6 b/test/javascripts/acceptance/admin-search-log-term-test.js.es6 index 9d3fbd6b43..36c7864215 100644 --- a/test/javascripts/acceptance/admin-search-log-term-test.js.es6 +++ b/test/javascripts/acceptance/admin-search-log-term-test.js.es6 @@ -4,8 +4,8 @@ acceptance("Admin - Search Log Term", { loggedIn: true }); QUnit.test("show search log term details", assert => { visit("/admin/logs/search_logs/term/ruby"); andThen(() => { - assert.ok($('div.search-logs-filter').length, "has the search type filter"); - assert.ok(exists('canvas.chartjs-render-monitor'), "has graph canvas"); - assert.ok(exists('div.header-search-results'), "has header search results"); + assert.ok($("div.search-logs-filter").length, "has the search type filter"); + assert.ok(exists("canvas.chartjs-render-monitor"), "has graph canvas"); + assert.ok(exists("div.header-search-results"), "has header search results"); }); }); diff --git a/test/javascripts/acceptance/admin-search-logs-test.js.es6 b/test/javascripts/acceptance/admin-search-logs-test.js.es6 index e1e7ed5355..2f8245f7f9 100644 --- a/test/javascripts/acceptance/admin-search-logs-test.js.es6 +++ b/test/javascripts/acceptance/admin-search-logs-test.js.es6 @@ -4,7 +4,10 @@ acceptance("Admin - Search Logs", { loggedIn: true }); QUnit.test("show search logs", assert => { visit("/admin/logs/search_logs"); andThen(() => { - assert.ok($('div.table.search-logs-list').length, "has the div class"); - assert.ok(exists('.search-logs-list .admin-list-item .col'), "has a list of search logs"); + assert.ok($("div.table.search-logs-list").length, "has the div class"); + assert.ok( + exists(".search-logs-list .admin-list-item .col"), + "has a list of search logs" + ); }); }); diff --git a/test/javascripts/acceptance/admin-site-text-test.js.es6 b/test/javascripts/acceptance/admin-site-text-test.js.es6 index c20dc09f4f..3146e0db41 100644 --- a/test/javascripts/acceptance/admin-site-text-test.js.es6 +++ b/test/javascripts/acceptance/admin-site-text-test.js.es6 @@ -5,50 +5,48 @@ acceptance("Admin - Site Texts", { loggedIn: true }); QUnit.test("search for a key", assert => { visit("/admin/customize/site_texts"); - fillIn('.site-text-search', 'Test'); + fillIn(".site-text-search", "Test"); andThen(() => { - assert.ok(exists('.site-text')); + assert.ok(exists(".site-text")); assert.ok(exists(".site-text:not(.overridden)")); - assert.ok(exists('.site-text.overridden')); + assert.ok(exists(".site-text.overridden")); }); - // Only show overridden - click('.extra-options input'); + click(".extra-options input"); andThen(() => { assert.ok(!exists(".site-text:not(.overridden)")); - assert.ok(exists('.site-text.overridden')); + assert.ok(exists(".site-text.overridden")); }); }); - QUnit.test("edit and revert a site text by key", assert => { visit("/admin/customize/site_texts/site.test"); andThen(() => { - assert.equal(find('.title h3').text(), 'site.test'); - assert.ok(!exists('.save-messages .saved')); - assert.ok(!exists('.save-messages .saved')); - assert.ok(!exists('.revert-site-text')); + assert.equal(find(".title h3").text(), "site.test"); + assert.ok(!exists(".save-messages .saved")); + assert.ok(!exists(".save-messages .saved")); + assert.ok(!exists(".revert-site-text")); }); // Change the value - fillIn('.site-text-value', 'New Test Value'); + fillIn(".site-text-value", "New Test Value"); click(".save-changes"); andThen(() => { - assert.ok(exists('.save-messages .saved')); - assert.ok(exists('.revert-site-text')); + assert.ok(exists(".save-messages .saved")); + assert.ok(exists(".revert-site-text")); }); // Revert the changes - click('.revert-site-text'); + click(".revert-site-text"); andThen(() => { - assert.ok(exists('.bootbox.modal')); + assert.ok(exists(".bootbox.modal")); }); - click('.bootbox.modal .btn-primary'); + click(".bootbox.modal .btn-primary"); andThen(() => { - assert.ok(!exists('.save-messages .saved')); - assert.ok(!exists('.revert-site-text')); + assert.ok(!exists(".save-messages .saved")); + assert.ok(!exists(".revert-site-text")); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 index cc67726886..055d890c05 100644 --- a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 +++ b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 @@ -4,17 +4,21 @@ acceptance("Admin - Suspend User", { loggedIn: true, pretend(server, helper) { - server.put('/admin/users/:user_id/suspend', () => helper.response(200, { - suspension: { - suspended: true - } - })); + server.put("/admin/users/:user_id/suspend", () => + helper.response(200, { + suspension: { + suspended: true + } + }) + ); - server.put('/admin/users/:user_id/unsuspend', () => helper.response(200, { - suspension: { - suspended: false - } - })); + server.put("/admin/users/:user_id/unsuspend", () => + helper.response(200, { + suspension: { + suspended: false + } + }) + ); } }); @@ -23,47 +27,55 @@ QUnit.test("suspend a user - cancel", assert => { click(".suspend-user"); andThen(() => { - assert.equal(find('.suspend-user-modal:visible').length, 1); + assert.equal(find(".suspend-user-modal:visible").length, 1); }); - click('.d-modal-cancel'); + click(".d-modal-cancel"); andThen(() => { - assert.equal(find('.suspend-user-modal:visible').length, 0); + assert.equal(find(".suspend-user-modal:visible").length, 0); }); }); QUnit.test("suspend, then unsuspend a user", assert => { - const suspendUntilCombobox = selectKit('.suspend-until .combobox'); + const suspendUntilCombobox = selectKit(".suspend-until .combobox"); visit("/admin/flags/active"); visit("/admin/users/1234/regular"); andThen(() => { - assert.ok(!exists('.suspension-info')); + assert.ok(!exists(".suspension-info")); }); click(".suspend-user"); andThen(() => { - assert.equal(find('.perform-suspend[disabled]').length, 1, 'disabled by default'); + assert.equal( + find(".perform-suspend[disabled]").length, + 1, + "disabled by default" + ); }); - suspendUntilCombobox.expand().selectRowByValue('tomorrow'); + suspendUntilCombobox.expand().selectRowByValue("tomorrow"); - fillIn('.suspend-reason', "for breaking the rules"); - fillIn('.suspend-message', "this is an email reason why"); + fillIn(".suspend-reason", "for breaking the rules"); + fillIn(".suspend-message", "this is an email reason why"); andThen(() => { - assert.equal(find('.perform-suspend[disabled]').length, 0, 'no longer disabled'); + assert.equal( + find(".perform-suspend[disabled]").length, + 0, + "no longer disabled" + ); }); - click('.perform-suspend'); + click(".perform-suspend"); andThen(() => { - assert.equal(find('.suspend-user-modal:visible').length, 0); - assert.ok(exists('.suspension-info')); + assert.equal(find(".suspend-user-modal:visible").length, 0); + assert.ok(exists(".suspension-info")); }); - click('.unsuspend-user'); + click(".unsuspend-user"); andThen(() => { - assert.ok(!exists('.suspension-info')); + assert.ok(!exists(".suspension-info")); }); }); diff --git a/test/javascripts/acceptance/admin-users-list-test.js.es6 b/test/javascripts/acceptance/admin-users-list-test.js.es6 index a6d3117e60..20fd19f20e 100644 --- a/test/javascripts/acceptance/admin-users-list-test.js.es6 +++ b/test/javascripts/acceptance/admin-users-list-test.js.es6 @@ -5,7 +5,7 @@ acceptance("Admin - Users List", { loggedIn: true }); QUnit.test("lists users", assert => { visit("/admin/users/list/active"); andThen(() => { - assert.ok(exists('.users-list .user')); - assert.ok(!exists('.user:eq(0) .email small'), 'escapes email'); + assert.ok(exists(".users-list .user")); + assert.ok(!exists(".user:eq(0) .email small"), "escapes email"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/admin-watched-words-test.js.es6 b/test/javascripts/acceptance/admin-watched-words-test.js.es6 index 88fd0728bd..9554705ce4 100644 --- a/test/javascripts/acceptance/admin-watched-words-test.js.es6 +++ b/test/javascripts/acceptance/admin-watched-words-test.js.es6 @@ -4,43 +4,60 @@ acceptance("Admin - Watched Words", { loggedIn: true }); QUnit.test("list words in groups", assert => { visit("/admin/logs/watched_words/action/block"); andThen(() => { - assert.ok(exists('.watched-words-list')); - assert.ok(!exists('.watched-words-list .watched-word'), "Don't show bad words by default."); + assert.ok(exists(".watched-words-list")); + assert.ok( + !exists(".watched-words-list .watched-word"), + "Don't show bad words by default." + ); }); - fillIn('.admin-controls .controls input[type=text]', 'li'); + fillIn(".admin-controls .controls input[type=text]", "li"); andThen(() => { - assert.equal(find('.watched-words-list .watched-word').length, 1, "When filtering, show words even if checkbox is unchecked."); + assert.equal( + find(".watched-words-list .watched-word").length, + 1, + "When filtering, show words even if checkbox is unchecked." + ); }); - fillIn('.admin-controls .controls input[type=text]', ''); + fillIn(".admin-controls .controls input[type=text]", ""); andThen(() => { - assert.ok(!exists('.watched-words-list .watched-word'), "Clearing the filter hides words again."); + assert.ok( + !exists(".watched-words-list .watched-word"), + "Clearing the filter hides words again." + ); }); - click('.show-words-checkbox'); + click(".show-words-checkbox"); andThen(() => { - assert.ok(exists('.watched-words-list .watched-word'), "Always show the words when checkbox is checked."); + assert.ok( + exists(".watched-words-list .watched-word"), + "Always show the words when checkbox is checked." + ); }); - click('.nav-stacked .censor a'); + click(".nav-stacked .censor a"); andThen(() => { - assert.ok(exists('.watched-words-list')); - assert.ok(!exists('.watched-words-list .watched-word'), "Empty word list."); + assert.ok(exists(".watched-words-list")); + assert.ok(!exists(".watched-words-list .watched-word"), "Empty word list."); }); }); QUnit.test("add words", assert => { visit("/admin/logs/watched_words/action/block"); andThen(() => { - click('.show-words-checkbox'); - fillIn('.watched-word-form input', 'poutine'); + click(".show-words-checkbox"); + fillIn(".watched-word-form input", "poutine"); }); - click('.watched-word-form button'); + click(".watched-word-form button"); andThen(() => { let found = []; - _.each(find('.watched-words-list .watched-word'), i => { - if ($(i).text().trim() === 'poutine') { + _.each(find(".watched-words-list .watched-word"), i => { + if ( + $(i) + .text() + .trim() === "poutine" + ) { found.push(true); } }); @@ -50,19 +67,22 @@ QUnit.test("add words", assert => { QUnit.test("remove words", assert => { visit("/admin/logs/watched_words/action/block"); - click('.show-words-checkbox'); + click(".show-words-checkbox"); let word = null; andThen(() => { - _.each(find('.watched-words-list .watched-word'), i => { - if ($(i).text().trim() === 'anise') { + _.each(find(".watched-words-list .watched-word"), i => { + if ( + $(i) + .text() + .trim() === "anise" + ) { word = i; } }); - click('#' + $(word).attr('id')); + click("#" + $(word).attr("id")); }); andThen(() => { - assert.equal(find('.watched-words-list .watched-word').length, 1); + assert.equal(find(".watched-words-list .watched-word").length, 1); }); }); - diff --git a/test/javascripts/acceptance/badges-test.js.es6 b/test/javascripts/acceptance/badges-test.js.es6 index 9052efe222..8ad8cc1f34 100644 --- a/test/javascripts/acceptance/badges-test.js.es6 +++ b/test/javascripts/acceptance/badges-test.js.es6 @@ -5,14 +5,14 @@ acceptance("Badges"); QUnit.test("Visit Badge Pages", assert => { visit("/badges"); andThen(() => { - assert.ok($('body.badges-page').length, "has body class"); - assert.ok(exists('.badge-groups .badge-card'), "has a list of badges"); + assert.ok($("body.badges-page").length, "has body class"); + assert.ok(exists(".badge-groups .badge-card"), "has a list of badges"); }); visit("/badges/9/autobiographer"); andThen(() => { - assert.ok(exists('.badge-card'), "has the badge in the listing"); - assert.ok(exists('.user-info'), "has the list of users with that badge"); - assert.ok(!exists('.badge-card:eq(0) script')); + assert.ok(exists(".badge-card"), "has the badge in the listing"); + assert.ok(exists(".user-info"), "has the list of users with that badge"); + assert.ok(!exists(".badge-card:eq(0) script")); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/category-chooser-test.js.es6 b/test/javascripts/acceptance/category-chooser-test.js.es6 index bd3979993f..13166969f4 100644 --- a/test/javascripts/acceptance/category-chooser-test.js.es6 +++ b/test/javascripts/acceptance/category-chooser-test.js.es6 @@ -1,29 +1,34 @@ -import { acceptance } from 'helpers/qunit-helpers'; +import { acceptance } from "helpers/qunit-helpers"; -acceptance('CategoryChooser', { +acceptance("CategoryChooser", { loggedIn: true, settings: { allow_uncategorized_topics: false } }); -QUnit.test('does not display uncategorized if not allowed', assert => { - const categoryChooser = selectKit('.category-chooser'); +QUnit.test("does not display uncategorized if not allowed", assert => { + const categoryChooser = selectKit(".category-chooser"); - visit('/'); - click('#create-topic'); + visit("/"); + click("#create-topic"); categoryChooser.expand(); andThen(() => { - assert.ok(categoryChooser.rowByIndex(0).name() !== 'uncategorized'); + assert.ok(categoryChooser.rowByIndex(0).name() !== "uncategorized"); }); }); -QUnit.test('prefill category when category_id is set', assert => { - visit('/new-topic?category_id=1'); +QUnit.test("prefill category when category_id is set", assert => { + visit("/new-topic?category_id=1"); andThen(() => { - assert.equal(selectKit('.category-chooser').header().value(), 1); + assert.equal( + selectKit(".category-chooser") + .header() + .value(), + 1 + ); }); }); diff --git a/test/javascripts/acceptance/category-edit-security-test.js.es6 b/test/javascripts/acceptance/category-edit-security-test.js.es6 index 2af84a1898..d73bc250be 100644 --- a/test/javascripts/acceptance/category-edit-security-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-security-test.js.es6 @@ -7,17 +7,23 @@ acceptance("Category Edit - security", { QUnit.test("default", assert => { visit("/c/bug"); - click('.edit-category'); - click('li.edit-category-security a'); + click(".edit-category"); + click("li.edit-category-security a"); andThen(() => { - const $permissionListItems = find('.permission-list li'); + const $permissionListItems = find(".permission-list li"); - const badgeName = $permissionListItems.eq(0).find('.badge-group').text(); - assert.equal(badgeName, 'everyone'); + const badgeName = $permissionListItems + .eq(0) + .find(".badge-group") + .text(); + assert.equal(badgeName, "everyone"); - const permission = $permissionListItems.eq(0).find('.permission').text(); - assert.equal(permission, 'Create / Reply / See'); + const permission = $permissionListItems + .eq(0) + .find(".permission") + .text(); + assert.equal(permission, "Create / Reply / See"); }); }); @@ -26,20 +32,28 @@ QUnit.test("removing a permission", assert => { visit("/c/bug"); - click('.edit-category'); - click('li.edit-category-security a'); - click('.edit-category-tab-security .edit-permission'); + click(".edit-category"); + click("li.edit-category-security a"); + click(".edit-category-tab-security .edit-permission"); availableGroups.expand(); andThen(() => { - assert.notOk(availableGroups.rowByValue('everyone').exists(), 'everyone is already used and is not in the available groups'); + assert.notOk( + availableGroups.rowByValue("everyone").exists(), + "everyone is already used and is not in the available groups" + ); }); - click('.edit-category-tab-security .permission-list li:first-of-type .remove-permission'); + click( + ".edit-category-tab-security .permission-list li:first-of-type .remove-permission" + ); availableGroups.expand(); andThen(() => { - assert.ok(availableGroups.rowByValue('everyone').exists(), 'everyone has been removed and appears in the available groups'); + assert.ok( + availableGroups.rowByValue("everyone").exists(), + "everyone has been removed and appears in the available groups" + ); }); }); @@ -49,21 +63,23 @@ QUnit.test("adding a permission", assert => { visit("/c/bug"); - click('.edit-category'); - click('li.edit-category-security a'); - click('.edit-category-tab-security .edit-permission'); - availableGroups.expand().selectRowByValue('staff'); - permissionSelector.expand().selectRowByValue('2'); - click('.edit-category-tab-security .add-permission'); + click(".edit-category"); + click("li.edit-category-security a"); + click(".edit-category-tab-security .edit-permission"); + availableGroups.expand().selectRowByValue("staff"); + permissionSelector.expand().selectRowByValue("2"); + click(".edit-category-tab-security .add-permission"); andThen(() => { - const $addedPermissionItem = find('.edit-category-tab-security .permission-list li:nth-child(2)'); + const $addedPermissionItem = find( + ".edit-category-tab-security .permission-list li:nth-child(2)" + ); - const badgeName = $addedPermissionItem.find('.badge-group').text(); - assert.equal(badgeName, 'staff'); + const badgeName = $addedPermissionItem.find(".badge-group").text(); + assert.equal(badgeName, "staff"); - const permission = $addedPermissionItem.find('.permission').text(); - assert.equal(permission, 'Reply / See'); + const permission = $addedPermissionItem.find(".permission").text(); + assert.equal(permission, "Reply / See"); }); }); @@ -72,27 +88,43 @@ QUnit.test("adding a previously removed permission", assert => { visit("/c/bug"); - click('.edit-category'); - click('li.edit-category-security a'); - click('.edit-category-tab-security .edit-permission'); - click('.edit-category-tab-security .permission-list li:first-of-type .remove-permission'); + click(".edit-category"); + click("li.edit-category-security a"); + click(".edit-category-tab-security .edit-permission"); + click( + ".edit-category-tab-security .permission-list li:first-of-type .remove-permission" + ); andThen(() => { - assert.equal(find(".edit-category-tab-security .permission-list li").length, 0, 'it removes the permission from the list'); + assert.equal( + find(".edit-category-tab-security .permission-list li").length, + 0, + "it removes the permission from the list" + ); }); - availableGroups.expand().selectRowByValue('everyone'); - click('.edit-category-tab-security .add-permission'); + availableGroups.expand().selectRowByValue("everyone"); + click(".edit-category-tab-security .add-permission"); andThen(() => { - assert.equal(find(".edit-category-tab-security .permission-list li").length, 1, 'it adds the permission to the list'); + assert.equal( + find(".edit-category-tab-security .permission-list li").length, + 1, + "it adds the permission to the list" + ); - const $permissionListItems = find('.permission-list li'); + const $permissionListItems = find(".permission-list li"); - const badgeName = $permissionListItems.eq(0).find('.badge-group').text(); - assert.equal(badgeName, 'everyone'); + const badgeName = $permissionListItems + .eq(0) + .find(".badge-group") + .text(); + assert.equal(badgeName, "everyone"); - const permission = $permissionListItems.eq(0).find('.permission').text(); - assert.equal(permission, 'Create / Reply / See'); + const permission = $permissionListItems + .eq(0) + .find(".permission") + .text(); + assert.equal(permission, "Create / Reply / See"); }); }); diff --git a/test/javascripts/acceptance/category-edit-test.js.es6 b/test/javascripts/acceptance/category-edit-test.js.es6 index 4f9d2da99f..9dedfa8283 100644 --- a/test/javascripts/acceptance/category-edit-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-test.js.es6 @@ -1,4 +1,4 @@ -import DiscourseURL from 'discourse/lib/url'; +import DiscourseURL from "discourse/lib/url"; import { acceptance } from "helpers/qunit-helpers"; acceptance("Category Edit", { @@ -9,79 +9,101 @@ acceptance("Category Edit", { QUnit.test("Can open the category modal", assert => { visit("/c/bug"); - click('.edit-category'); + click(".edit-category"); andThen(() => { - assert.ok(visible('.d-modal'), 'it pops up a modal'); + assert.ok(visible(".d-modal"), "it pops up a modal"); }); - click('a.close'); + click("a.close"); andThen(() => { - assert.ok(!visible('.d-modal'), 'it closes the modal'); + assert.ok(!visible(".d-modal"), "it closes the modal"); }); }); QUnit.test("Change the category color", assert => { visit("/c/bug"); - click('.edit-category'); - fillIn('#edit-text-color', '#ff0000'); - click('#save-category'); + click(".edit-category"); + fillIn("#edit-text-color", "#ff0000"); + click("#save-category"); andThen(() => { - assert.ok(!visible('.d-modal'), 'it closes the modal'); - assert.equal(DiscourseURL.redirectedTo, '/c/bug', 'it does one of the rare full page redirects'); + assert.ok(!visible(".d-modal"), "it closes the modal"); + assert.equal( + DiscourseURL.redirectedTo, + "/c/bug", + "it does one of the rare full page redirects" + ); }); }); QUnit.test("Change the topic template", assert => { visit("/c/bug"); - click('.edit-category'); - click('.edit-category-topic-template'); - fillIn('.d-editor-input', 'this is the new topic template'); - click('#save-category'); + click(".edit-category"); + click(".edit-category-topic-template"); + fillIn(".d-editor-input", "this is the new topic template"); + click("#save-category"); andThen(() => { - assert.ok(!visible('.d-modal'), 'it closes the modal'); - assert.equal(DiscourseURL.redirectedTo, '/c/bug', 'it does one of the rare full page redirects'); + assert.ok(!visible(".d-modal"), "it closes the modal"); + assert.equal( + DiscourseURL.redirectedTo, + "/c/bug", + "it does one of the rare full page redirects" + ); }); }); QUnit.test("Error Saving", assert => { visit("/c/bug"); - click('.edit-category'); - click('.edit-category-settings'); - fillIn('.email-in', 'duplicate@example.com'); - click('#save-category'); + click(".edit-category"); + click(".edit-category-settings"); + fillIn(".email-in", "duplicate@example.com"); + click("#save-category"); andThen(() => { - assert.ok(visible('#modal-alert')); - assert.equal(find('#modal-alert').html(), "duplicate email"); + assert.ok(visible("#modal-alert")); + assert.equal(find("#modal-alert").html(), "duplicate email"); }); }); QUnit.test("Subcategory list settings", assert => { - const categoryChooser = selectKit('.edit-category-tab-general .category-chooser'); + const categoryChooser = selectKit( + ".edit-category-tab-general .category-chooser" + ); visit("/c/bug"); - click('.edit-category'); - click('.edit-category-settings'); + click(".edit-category"); + click(".edit-category-settings"); andThen(() => { - assert.ok(!visible(".subcategory-list-style-field"), "subcategory list style isn't visible by default"); + assert.ok( + !visible(".subcategory-list-style-field"), + "subcategory list style isn't visible by default" + ); }); click(".show-subcategory-list-field input[type=checkbox]"); andThen(() => { - assert.ok(visible(".subcategory-list-style-field"), "subcategory list style is shown if show subcategory list is checked"); + assert.ok( + visible(".subcategory-list-style-field"), + "subcategory list style is shown if show subcategory list is checked" + ); }); - click('.edit-category-general'); + click(".edit-category-general"); categoryChooser.expand().selectRowByValue(3); - click('.edit-category-settings'); + click(".edit-category-settings"); andThen(() => { - assert.ok(!visible(".show-subcategory-list-field"), "show subcategory list isn't visible for child categories"); - assert.ok(!visible(".subcategory-list-style-field"), "subcategory list style isn't visible for child categories"); + assert.ok( + !visible(".show-subcategory-list-field"), + "show subcategory list isn't visible for child categories" + ); + assert.ok( + !visible(".subcategory-list-style-field"), + "subcategory list style isn't visible for child categories" + ); }); }); diff --git a/test/javascripts/acceptance/category-hashtag-test.js.es6 b/test/javascripts/acceptance/category-hashtag-test.js.es6 index bd410abc18..45864748b3 100644 --- a/test/javascripts/acceptance/category-hashtag-test.js.es6 +++ b/test/javascripts/acceptance/category-hashtag-test.js.es6 @@ -4,16 +4,26 @@ acceptance("Category hashtag", { loggedIn: true }); QUnit.test("category hashtag is cooked properly", assert => { visit("/t/internationalization-localization/280"); - click('#topic-footer-buttons .btn.create'); + click("#topic-footer-buttons .btn.create"); - fillIn('.d-editor-input', "this is a category hashtag #bug"); + fillIn(".d-editor-input", "this is a category hashtag #bug"); andThen(() => { // TODO: Test that the autocomplete shows - assert.equal(find('.d-editor-preview:visible').html().trim(), "

this is a category hashtag #bug

"); + assert.equal( + find(".d-editor-preview:visible") + .html() + .trim(), + '

this is a category hashtag #bug

' + ); }); - click('#reply-control .btn.create'); + click("#reply-control .btn.create"); andThen(() => { - assert.equal(find('.topic-post:last .cooked p').html().trim(), "this is a category hashtag #bug"); + assert.equal( + find(".topic-post:last .cooked p") + .html() + .trim(), + 'this is a category hashtag #bug' + ); }); }); diff --git a/test/javascripts/acceptance/composer-actions-test.js.es6 b/test/javascripts/acceptance/composer-actions-test.js.es6 index cf9ad80e0e..11517d60d3 100644 --- a/test/javascripts/acceptance/composer-actions-test.js.es6 +++ b/test/javascripts/acceptance/composer-actions-test.js.es6 @@ -1,173 +1,245 @@ -import { acceptance } from 'helpers/qunit-helpers'; -import { _clearSnapshots } from 'select-kit/components/composer-actions'; +import { acceptance } from "helpers/qunit-helpers"; +import { _clearSnapshots } from "select-kit/components/composer-actions"; -acceptance('Composer Actions', { +acceptance("Composer Actions", { loggedIn: true, settings: { enable_whispers: true }, beforeEach() { _clearSnapshots(); - }, + } }); -QUnit.test('replying to post', async assert => { - const composerActions = selectKit('.composer-actions'); +QUnit.test("replying to post", async assert => { + const composerActions = selectKit(".composer-actions"); - await visit('/t/internationalization-localization/280'); - await click('article#post_3 button.reply'); + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); await composerActions.expandAwait(); - assert.equal(composerActions.rowByIndex(0).value(), 'reply_as_new_topic'); - assert.equal(composerActions.rowByIndex(1).value(), 'reply_as_private_message'); - assert.equal(composerActions.rowByIndex(2).value(), 'reply_to_topic'); - assert.equal(composerActions.rowByIndex(3).value(), 'toggle_whisper'); + assert.equal(composerActions.rowByIndex(0).value(), "reply_as_new_topic"); + assert.equal( + composerActions.rowByIndex(1).value(), + "reply_as_private_message" + ); + assert.equal(composerActions.rowByIndex(2).value(), "reply_to_topic"); + assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); assert.equal(composerActions.rowByIndex(4).value(), undefined); }); -QUnit.test('replying to post - reply_as_private_message', async assert => { - const composerActions = selectKit('.composer-actions'); +QUnit.test("replying to post - reply_as_private_message", async assert => { + const composerActions = selectKit(".composer-actions"); - await visit('/t/internationalization-localization/280'); - await click('article#post_3 button.reply'); + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait('reply_as_private_message'); - - assert.equal(find('.users-input .item:eq(0)').text(), 'codinghorror'); - assert.ok(find('.d-editor-input').val().indexOf('Continuing the discussion') >= 0); -}); - -QUnit.test('replying to post - reply_to_topic', async assert => { - const composerActions = selectKit('.composer-actions'); - - await visit('/t/internationalization-localization/280'); - await click('article#post_3 button.reply'); - await fillIn('.d-editor-input', 'test replying to topic when initially replied to post'); - - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait('reply_to_topic'); - - assert.equal(find('.action-title .topic-link').text().trim(), 'Internationalization / localization'); - assert.equal(find('.action-title .topic-link').attr("href"), '/t/internationalization-localization/280'); - assert.equal(find('.d-editor-input').val(), 'test replying to topic when initially replied to post'); -}); - -QUnit.test('replying to post - toggle_whisper', async assert => { - const composerActions = selectKit('.composer-actions'); - - await visit('/t/internationalization-localization/280'); - await click('article#post_3 button.reply'); - await fillIn('.d-editor-input', 'test replying as whisper to topic when initially not a whisper'); - - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait('toggle_whisper'); + await composerActions.selectRowByValueAwait("reply_as_private_message"); + assert.equal(find(".users-input .item:eq(0)").text(), "codinghorror"); assert.ok( - find('.composer-fields .whisper').text().indexOf(I18n.t("composer.whisper")) > 0 + find(".d-editor-input") + .val() + .indexOf("Continuing the discussion") >= 0 ); }); -QUnit.test('replying to post - reply_as_new_topic', async assert => { - const composerActions = selectKit('.composer-actions'); - const categoryChooser = selectKit('.title-wrapper .category-chooser'); - const categoryChooserReplyArea = selectKit('.reply-area .category-chooser'); - const quote = 'test replying as new topic when initially replied to post'; +QUnit.test("replying to post - reply_to_topic", async assert => { + const composerActions = selectKit(".composer-actions"); - await visit('/t/internationalization-localization/280'); - - await click('#topic-title .d-icon-pencil'); - await categoryChooser.expandAwait(); - await categoryChooser.selectRowByValueAwait(4); - await click('#topic-title .submit-edit'); - - await click('article#post_3 button.reply'); - await fillIn('.d-editor-input', quote); + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); + await fillIn( + ".d-editor-input", + "test replying to topic when initially replied to post" + ); await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait('reply_as_new_topic'); - - assert.equal(categoryChooserReplyArea.header().name(), 'faq'); - assert.equal(find('.action-title').text().trim(), I18n.t("topic.create_long")); - assert.ok(find('.d-editor-input').val().includes(quote)); -}); - -QUnit.test('shared draft', async assert => { - const composerActions = selectKit('.composer-actions'); - - await visit("/"); - await click('#create-topic'); - - await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait('shared_draft'); + await composerActions.selectRowByValueAwait("reply_to_topic"); assert.equal( - find('#reply-control .btn-primary.create .d-button-label').text(), - I18n.t('composer.create_shared_draft') + find(".action-title .topic-link") + .text() + .trim(), + "Internationalization / localization" + ); + assert.equal( + find(".action-title .topic-link").attr("href"), + "/t/internationalization-localization/280" + ); + assert.equal( + find(".d-editor-input").val(), + "test replying to topic when initially replied to post" ); - assert.ok(find('#reply-control.composing-shared-draft').length === 1); }); -QUnit.test('hide component if no content', async assert => { - const composerActions = selectKit('.composer-actions'); +QUnit.test("replying to post - toggle_whisper", async assert => { + const composerActions = selectKit(".composer-actions"); - await visit('/u/eviltrout/messages'); - await click('.new-private-message'); + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); + await fillIn( + ".d-editor-input", + "test replying as whisper to topic when initially not a whisper" + ); + + await composerActions.expandAwait(); + await composerActions.selectRowByValueAwait("toggle_whisper"); + + assert.ok( + find(".composer-fields .whisper") + .text() + .indexOf(I18n.t("composer.whisper")) > 0 + ); +}); + +QUnit.test("replying to post - reply_as_new_topic", async assert => { + const composerActions = selectKit(".composer-actions"); + const categoryChooser = selectKit(".title-wrapper .category-chooser"); + const categoryChooserReplyArea = selectKit(".reply-area .category-chooser"); + const quote = "test replying as new topic when initially replied to post"; + + await visit("/t/internationalization-localization/280"); + + await click("#topic-title .d-icon-pencil"); + await categoryChooser.expandAwait(); + await categoryChooser.selectRowByValueAwait(4); + await click("#topic-title .submit-edit"); + + await click("article#post_3 button.reply"); + await fillIn(".d-editor-input", quote); + + await composerActions.expandAwait(); + await composerActions.selectRowByValueAwait("reply_as_new_topic"); + + assert.equal(categoryChooserReplyArea.header().name(), "faq"); + assert.equal( + find(".action-title") + .text() + .trim(), + I18n.t("topic.create_long") + ); + assert.ok( + find(".d-editor-input") + .val() + .includes(quote) + ); +}); + +QUnit.test("shared draft", async assert => { + const composerActions = selectKit(".composer-actions"); + + await visit("/"); + await click("#create-topic"); + + await composerActions.expandAwait(); + await composerActions.selectRowByValueAwait("shared_draft"); + + assert.equal( + find("#reply-control .btn-primary.create .d-button-label").text(), + I18n.t("composer.create_shared_draft") + ); + assert.ok(find("#reply-control.composing-shared-draft").length === 1); +}); + +QUnit.test("hide component if no content", async assert => { + const composerActions = selectKit(".composer-actions"); + + await visit("/u/eviltrout/messages"); + await click(".new-private-message"); assert.ok(composerActions.el().hasClass("is-hidden")); }); -QUnit.test('interactions', async assert => { - const composerActions = selectKit('.composer-actions'); - const quote = 'Life is like riding a bicycle.'; +QUnit.test("interactions", async assert => { + const composerActions = selectKit(".composer-actions"); + const quote = "Life is like riding a bicycle."; - await visit('/t/internationalization-localization/280'); - await click('article#post_3 button.reply'); - await fillIn('.d-editor-input', quote); + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); + await fillIn(".d-editor-input", quote); await composerActions.expandAwait(); - await composerActions.selectRowByValueAwait('reply_to_topic'); + await composerActions.selectRowByValueAwait("reply_to_topic"); - assert.equal(find('.action-title').text().trim(), "Internationalization / localization"); - assert.equal(find('.d-editor-input').val(), quote); + assert.equal( + find(".action-title") + .text() + .trim(), + "Internationalization / localization" + ); + assert.equal(find(".d-editor-input").val(), quote); await composerActions.expandAwait(); - assert.equal(composerActions.rowByIndex(0).value(), 'reply_as_new_topic'); - assert.equal(composerActions.rowByIndex(1).value(), 'reply_to_post'); - assert.equal(composerActions.rowByIndex(2).value(), 'reply_as_private_message'); - assert.equal(composerActions.rowByIndex(3).value(), 'toggle_whisper'); + assert.equal(composerActions.rowByIndex(0).value(), "reply_as_new_topic"); + assert.equal(composerActions.rowByIndex(1).value(), "reply_to_post"); + assert.equal( + composerActions.rowByIndex(2).value(), + "reply_as_private_message" + ); + assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); assert.equal(composerActions.rows().length, 4); - await composerActions.selectRowByValueAwait('reply_to_post'); + await composerActions.selectRowByValueAwait("reply_to_post"); await composerActions.expandAwait(); - assert.ok(exists(find('.action-title img.avatar'))); - assert.equal(find('.action-title .user-link').text().trim(), "codinghorror"); - assert.equal(find('.d-editor-input').val(), quote); - assert.equal(composerActions.rowByIndex(0).value(), 'reply_as_new_topic'); - assert.equal(composerActions.rowByIndex(1).value(), 'reply_as_private_message'); - assert.equal(composerActions.rowByIndex(2).value(), 'reply_to_topic'); - assert.equal(composerActions.rowByIndex(3).value(), 'toggle_whisper'); + assert.ok(exists(find(".action-title img.avatar"))); + assert.equal( + find(".action-title .user-link") + .text() + .trim(), + "codinghorror" + ); + assert.equal(find(".d-editor-input").val(), quote); + assert.equal(composerActions.rowByIndex(0).value(), "reply_as_new_topic"); + assert.equal( + composerActions.rowByIndex(1).value(), + "reply_as_private_message" + ); + assert.equal(composerActions.rowByIndex(2).value(), "reply_to_topic"); + assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); assert.equal(composerActions.rows().length, 4); - await composerActions.selectRowByValueAwait('reply_as_new_topic'); + await composerActions.selectRowByValueAwait("reply_as_new_topic"); await composerActions.expandAwait(); - assert.equal(find('.action-title').text().trim(), I18n.t("topic.create_long")); - assert.ok(find('.d-editor-input').val().includes(quote)); - assert.equal(composerActions.rowByIndex(0).value(), 'reply_to_post'); - assert.equal(composerActions.rowByIndex(1).value(), 'reply_as_private_message'); - assert.equal(composerActions.rowByIndex(2).value(), 'reply_to_topic'); - assert.equal(composerActions.rowByIndex(3).value(), 'shared_draft'); + assert.equal( + find(".action-title") + .text() + .trim(), + I18n.t("topic.create_long") + ); + assert.ok( + find(".d-editor-input") + .val() + .includes(quote) + ); + assert.equal(composerActions.rowByIndex(0).value(), "reply_to_post"); + assert.equal( + composerActions.rowByIndex(1).value(), + "reply_as_private_message" + ); + assert.equal(composerActions.rowByIndex(2).value(), "reply_to_topic"); + assert.equal(composerActions.rowByIndex(3).value(), "shared_draft"); assert.equal(composerActions.rows().length, 4); - await composerActions.selectRowByValueAwait('reply_as_private_message'); + await composerActions.selectRowByValueAwait("reply_as_private_message"); await composerActions.expandAwait(); - assert.equal(find('.action-title').text().trim(), I18n.t("topic.private_message")); - assert.ok(find('.d-editor-input').val().indexOf("Continuing the discussion") === 0); - assert.equal(composerActions.rowByIndex(0).value(), 'reply_as_new_topic'); - assert.equal(composerActions.rowByIndex(1).value(), 'reply_to_post'); - assert.equal(composerActions.rowByIndex(2).value(), 'reply_to_topic'); + assert.equal( + find(".action-title") + .text() + .trim(), + I18n.t("topic.private_message") + ); + assert.ok( + find(".d-editor-input") + .val() + .indexOf("Continuing the discussion") === 0 + ); + assert.equal(composerActions.rowByIndex(0).value(), "reply_as_new_topic"); + assert.equal(composerActions.rowByIndex(1).value(), "reply_to_post"); + assert.equal(composerActions.rowByIndex(2).value(), "reply_to_topic"); assert.equal(composerActions.rows().length, 3); }); diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 index bd60b6cbb7..a42fb87aa9 100644 --- a/test/javascripts/acceptance/composer-test.js.es6 +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -10,53 +10,80 @@ acceptance("Composer", { QUnit.test("Tests the Composer controls", assert => { visit("/"); andThen(() => { - assert.ok(exists('#create-topic'), 'the create button is visible'); + assert.ok(exists("#create-topic"), "the create button is visible"); }); - click('#create-topic'); + click("#create-topic"); andThen(() => { - assert.ok(exists('.d-editor-input'), 'the composer input is visible'); - assert.ok(exists('.title-input .popup-tip.bad.hide'), 'title errors are hidden by default'); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.bad.hide'), 'body errors are hidden by default'); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); + assert.ok( + exists(".title-input .popup-tip.bad.hide"), + "title errors are hidden by default" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.bad.hide"), + "body errors are hidden by default" + ); }); - click('a.toggle-preview'); + click("a.toggle-preview"); andThen(() => { - assert.ok(!exists('.d-editor-preview:visible'), "clicking the toggle hides the preview"); + assert.ok( + !exists(".d-editor-preview:visible"), + "clicking the toggle hides the preview" + ); }); - click('a.toggle-preview'); + click("a.toggle-preview"); andThen(() => { - assert.ok(exists('.d-editor-preview:visible'), "clicking the toggle shows the preview again"); + assert.ok( + exists(".d-editor-preview:visible"), + "clicking the toggle shows the preview again" + ); }); - click('#reply-control button.create'); + click("#reply-control button.create"); andThen(() => { - assert.ok(!exists('.title-input .popup-tip.bad.hide'), 'it shows the empty title error'); - assert.ok(!exists('.d-editor-wrapper .popup-tip.bad.hide'), 'it shows the empty body error'); + assert.ok( + !exists(".title-input .popup-tip.bad.hide"), + "it shows the empty title error" + ); + assert.ok( + !exists(".d-editor-wrapper .popup-tip.bad.hide"), + "it shows the empty body error" + ); }); - fillIn('#reply-title', "this is my new topic title"); + fillIn("#reply-title", "this is my new topic title"); andThen(() => { - assert.ok(exists('.title-input .popup-tip.good'), 'the title is now good'); + assert.ok(exists(".title-input .popup-tip.good"), "the title is now good"); }); - fillIn('.d-editor-input', "this is the *content* of a post"); + fillIn(".d-editor-input", "this is the *content* of a post"); andThen(() => { - assert.equal(find('.d-editor-preview').html().trim(), "

this is the content of a post

", "it previews content"); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good'); + assert.equal( + find(".d-editor-preview") + .html() + .trim(), + "

this is the content of a post

", + "it previews content" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.good"), + "the body is now good" + ); }); andThen(() => { - const textarea = find('#reply-control .d-editor-input')[0]; + const textarea = find("#reply-control .d-editor-input")[0]; textarea.selectionStart = textarea.value.length; textarea.selectionEnd = textarea.value.length; // Testing keyboard events is tough! const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); - const event = document.createEvent('Event'); - event.initEvent('keydown', true, true); - event[mac ? 'metaKey' : 'ctrlKey'] = true; + const event = document.createEvent("Event"); + event.initEvent("keydown", true, true); + event[mac ? "metaKey" : "ctrlKey"] = true; event.keyCode = 66; textarea.dispatchEvent(event); @@ -64,242 +91,329 @@ QUnit.test("Tests the Composer controls", assert => { andThen(() => { const example = I18n.t(`composer.bold_text`); - assert.equal(find('#reply-control .d-editor-input').val().trim(), - `this is the *content* of a post**${example}**`, - "it supports keyboard shortcuts"); + assert.equal( + find("#reply-control .d-editor-input") + .val() + .trim(), + `this is the *content* of a post**${example}**`, + "it supports keyboard shortcuts" + ); }); - click('#reply-control a.cancel'); + click("#reply-control a.cancel"); andThen(() => { - assert.ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); + assert.ok(exists(".bootbox.modal"), "it pops up a confirmation dialog"); }); - click('.modal-footer a:eq(1)'); + click(".modal-footer a:eq(1)"); andThen(() => { - assert.ok(!exists('.bootbox.modal'), 'the confirmation can be cancelled'); + assert.ok(!exists(".bootbox.modal"), "the confirmation can be cancelled"); }); - }); QUnit.test("Create a topic with server side errors", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', "this title triggers an error"); - fillIn('.d-editor-input', "this is the *content* of a post"); - click('#reply-control button.create'); + click("#create-topic"); + fillIn("#reply-title", "this title triggers an error"); + fillIn(".d-editor-input", "this is the *content* of a post"); + click("#reply-control button.create"); andThen(() => { - assert.ok(exists('.bootbox.modal'), 'it pops up an error message'); + assert.ok(exists(".bootbox.modal"), "it pops up an error message"); }); - click('.bootbox.modal a.btn-primary'); + click(".bootbox.modal a.btn-primary"); andThen(() => { - assert.ok(!exists('.bootbox.modal'), 'it dismisses the error'); - assert.ok(exists('.d-editor-input'), 'the composer input is visible'); + assert.ok(!exists(".bootbox.modal"), "it dismisses the error"); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); }); }); QUnit.test("Create a Topic", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', "Internationalization Localization"); - fillIn('.d-editor-input', "this is the *content* of a new topic post"); - click('#reply-control button.create'); + click("#create-topic"); + fillIn("#reply-title", "Internationalization Localization"); + fillIn(".d-editor-input", "this is the *content* of a new topic post"); + click("#reply-control button.create"); andThen(() => { - assert.equal(currentURL(), "/t/internationalization-localization/280", "it transitions to the newly created topic URL"); + assert.equal( + currentURL(), + "/t/internationalization-localization/280", + "it transitions to the newly created topic URL" + ); }); }); QUnit.test("Create an enqueued Topic", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', "Internationalization Localization"); - fillIn('.d-editor-input', "enqueue this content please"); - click('#reply-control button.create'); + click("#create-topic"); + fillIn("#reply-title", "Internationalization Localization"); + fillIn(".d-editor-input", "enqueue this content please"); + click("#reply-control button.create"); andThen(() => { - assert.ok(visible('.d-modal'), 'it pops up a modal'); + assert.ok(visible(".d-modal"), "it pops up a modal"); assert.equal(currentURL(), "/", "it doesn't change routes"); }); - click('.modal-footer button'); + click(".modal-footer button"); andThen(() => { - assert.ok(invisible('.d-modal'), 'the modal can be dismissed'); + assert.ok(invisible(".d-modal"), "the modal can be dismissed"); }); }); - QUnit.test("Create a Reply", assert => { visit("/t/internationalization-localization/280"); andThen(() => { - assert.ok(!exists('article[data-post-id=12345]'), 'the post is not in the DOM'); + assert.ok( + !exists("article[data-post-id=12345]"), + "the post is not in the DOM" + ); }); - click('#topic-footer-buttons .btn.create'); + click("#topic-footer-buttons .btn.create"); andThen(() => { - assert.ok(exists('.d-editor-input'), 'the composer input is visible'); - assert.ok(!exists('#reply-title'), 'there is no title since this is a reply'); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); + assert.ok( + !exists("#reply-title"), + "there is no title since this is a reply" + ); }); - fillIn('.d-editor-input', 'this is the content of my reply'); - click('#reply-control button.create'); + fillIn(".d-editor-input", "this is the content of my reply"); + click("#reply-control button.create"); andThen(() => { - assert.equal(find('.cooked:last p').text(), 'this is the content of my reply'); + assert.equal( + find(".cooked:last p").text(), + "this is the content of my reply" + ); }); }); -QUnit.test("Posting on a different topic", (assert) => { +QUnit.test("Posting on a different topic", assert => { visit("/t/internationalization-localization/280"); - click('#topic-footer-buttons .btn.create'); - fillIn('.d-editor-input', 'this is the content for a different topic'); + click("#topic-footer-buttons .btn.create"); + fillIn(".d-editor-input", "this is the content for a different topic"); visit("/t/1-3-0beta9-no-rate-limit-popups/28830"); andThen(function() { assert.equal(currentURL(), "/t/1-3-0beta9-no-rate-limit-popups/28830"); }); - click('#reply-control button.create'); + click("#reply-control button.create"); andThen(function() { - assert.ok(visible('.reply-where-modal'), 'it pops up a modal'); + assert.ok(visible(".reply-where-modal"), "it pops up a modal"); }); - click('.btn-reply-here'); + click(".btn-reply-here"); andThen(() => { - assert.equal(find('.cooked:last p').text(), 'this is the content for a different topic'); + assert.equal( + find(".cooked:last p").text(), + "this is the content for a different topic" + ); }); }); - QUnit.test("Create an enqueued Reply", assert => { visit("/t/internationalization-localization/280"); - click('#topic-footer-buttons .btn.create'); + click("#topic-footer-buttons .btn.create"); andThen(() => { - assert.ok(exists('.d-editor-input'), 'the composer input is visible'); - assert.ok(!exists('#reply-title'), 'there is no title since this is a reply'); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); + assert.ok( + !exists("#reply-title"), + "there is no title since this is a reply" + ); }); - fillIn('.d-editor-input', 'enqueue this content please'); - click('#reply-control button.create'); + fillIn(".d-editor-input", "enqueue this content please"); + click("#reply-control button.create"); andThen(() => { - assert.ok(find('.cooked:last p').text() !== 'enqueue this content please', "it doesn't insert the post"); + assert.ok( + find(".cooked:last p").text() !== "enqueue this content please", + "it doesn't insert the post" + ); }); andThen(() => { - assert.ok(visible('.d-modal'), 'it pops up a modal'); + assert.ok(visible(".d-modal"), "it pops up a modal"); }); - click('.modal-footer button'); + click(".modal-footer button"); andThen(() => { - assert.ok(invisible('.d-modal'), 'the modal can be dismissed'); + assert.ok(invisible(".d-modal"), "the modal can be dismissed"); }); }); QUnit.test("Edit the first post", assert => { visit("/t/internationalization-localization/280"); - assert.ok(!exists('.topic-post:eq(0) .post-info.edits'), 'it has no edits icon at first'); + assert.ok( + !exists(".topic-post:eq(0) .post-info.edits"), + "it has no edits icon at first" + ); - click('.topic-post:eq(0) button.show-more-actions'); - click('.topic-post:eq(0) button.edit'); + click(".topic-post:eq(0) button.show-more-actions"); + click(".topic-post:eq(0) button.edit"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("Any plans to support"), + 0, + "it populates the input with the post text" + ); }); - fillIn('.d-editor-input', "This is the new text for the post"); - fillIn('#reply-title', "This is the new text for the title"); - click('#reply-control button.create'); + fillIn(".d-editor-input", "This is the new text for the post"); + fillIn("#reply-title", "This is the new text for the title"); + click("#reply-control button.create"); andThen(() => { - assert.ok(!exists('.d-editor-input'), 'it closes the composer'); - assert.ok(exists('.topic-post:eq(0) .post-info.edits'), 'it has the edits icon'); - assert.ok(find('#topic-title h1').text().indexOf('This is the new text for the title') !== -1, 'it shows the new title'); - assert.ok(find('.topic-post:eq(0) .cooked').text().indexOf('This is the new text for the post') !== -1, 'it updates the post'); + assert.ok(!exists(".d-editor-input"), "it closes the composer"); + assert.ok( + exists(".topic-post:eq(0) .post-info.edits"), + "it has the edits icon" + ); + assert.ok( + find("#topic-title h1") + .text() + .indexOf("This is the new text for the title") !== -1, + "it shows the new title" + ); + assert.ok( + find(".topic-post:eq(0) .cooked") + .text() + .indexOf("This is the new text for the post") !== -1, + "it updates the post" + ); }); }); QUnit.test("Composer can switch between edits", assert => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button.edit'); + click(".topic-post:eq(0) button.edit"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("This is the first post."), + 0, + "it populates the input with the post text" + ); }); - click('.topic-post:eq(1) button.edit'); + click(".topic-post:eq(1) button.edit"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('This is the second post.'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("This is the second post."), + 0, + "it populates the input with the post text" + ); }); }); QUnit.test("Composer with dirty edit can toggle to another edit", assert => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button.edit'); - fillIn('.d-editor-input', 'This is a dirty reply'); - click('.topic-post:eq(1) button.edit'); + click(".topic-post:eq(0) button.edit"); + fillIn(".d-editor-input", "This is a dirty reply"); + click(".topic-post:eq(1) button.edit"); andThen(() => { - assert.ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); + assert.ok(exists(".bootbox.modal"), "it pops up a confirmation dialog"); }); - click('.modal-footer a:eq(0)'); + click(".modal-footer a:eq(0)"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('This is the second post.'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("This is the second post."), + 0, + "it populates the input with the post text" + ); }); }); QUnit.test("Composer can toggle between edit and reply", assert => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button.edit'); + click(".topic-post:eq(0) button.edit"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("This is the first post."), + 0, + "it populates the input with the post text" + ); }); - click('.topic-post:eq(0) button.reply'); + click(".topic-post:eq(0) button.reply"); andThen(() => { - assert.equal(find('.d-editor-input').val(), "", 'it clears the input'); + assert.equal(find(".d-editor-input").val(), "", "it clears the input"); }); - click('.topic-post:eq(0) button.edit'); + click(".topic-post:eq(0) button.edit"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("This is the first post."), + 0, + "it populates the input with the post text" + ); }); }); QUnit.test("Composer can toggle between reply and createTopic", assert => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button.reply'); + click(".topic-post:eq(0) button.reply"); - selectKit('.toolbar-popup-menu-options').expand().selectRowByValue('toggleWhisper'); + selectKit(".toolbar-popup-menu-options") + .expand() + .selectRowByValue("toggleWhisper"); andThen(() => { assert.ok( - find('.composer-fields .whisper').text().indexOf(I18n.t("composer.whisper")) > 0, - 'it sets the post type to whisper' + find(".composer-fields .whisper") + .text() + .indexOf(I18n.t("composer.whisper")) > 0, + "it sets the post type to whisper" ); }); visit("/"); andThen(() => { - assert.ok(exists('#create-topic'), 'the create topic button is visible'); + assert.ok(exists("#create-topic"), "the create topic button is visible"); }); - click('#create-topic'); + click("#create-topic"); andThen(() => { assert.ok( - find('.composer-fields .whisper').text().indexOf(I18n.t("composer.whisper")) === -1, + find(".composer-fields .whisper") + .text() + .indexOf(I18n.t("composer.whisper")) === -1, "it should reset the state of the composer's model" ); }); - selectKit('.toolbar-popup-menu-options').expand().selectRowByValue('toggleInvisible'); + selectKit(".toolbar-popup-menu-options") + .expand() + .selectRowByValue("toggleInvisible"); andThen(() => { assert.ok( - find('.composer-fields .whisper').text().indexOf(I18n.t("composer.unlist")) > 0, - 'it sets the topic to unlisted' + find(".composer-fields .whisper") + .text() + .indexOf(I18n.t("composer.unlist")) > 0, + "it sets the topic to unlisted" ); }); visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button.reply'); + click(".topic-post:eq(0) button.reply"); andThen(() => { assert.ok( - find('.composer-fields .whisper').text().indexOf(I18n.t("composer.unlist")) === -1, + find(".composer-fields .whisper") + .text() + .indexOf(I18n.t("composer.unlist")) === -1, "it should reset the state of the composer's model" ); }); @@ -308,31 +422,43 @@ QUnit.test("Composer can toggle between reply and createTopic", assert => { QUnit.test("Composer with dirty reply can toggle to edit", assert => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button.reply'); - fillIn('.d-editor-input', 'This is a dirty reply'); - click('.topic-post:eq(0) button.edit'); + click(".topic-post:eq(0) button.reply"); + fillIn(".d-editor-input", "This is a dirty reply"); + click(".topic-post:eq(0) button.edit"); andThen(() => { - assert.ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); + assert.ok(exists(".bootbox.modal"), "it pops up a confirmation dialog"); }); - click('.modal-footer a:eq(0)'); + click(".modal-footer a:eq(0)"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("This is the first post."), + 0, + "it populates the input with the post text" + ); }); }); QUnit.test("Composer draft with dirty reply can toggle to edit", assert => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button.reply'); - fillIn('.d-editor-input', 'This is a dirty reply'); - click('.toggler'); - click('.topic-post:eq(0) button.edit'); + click(".topic-post:eq(0) button.reply"); + fillIn(".d-editor-input", "This is a dirty reply"); + click(".toggler"); + click(".topic-post:eq(0) button.edit"); andThen(() => { - assert.ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); + assert.ok(exists(".bootbox.modal"), "it pops up a confirmation dialog"); }); - click('.modal-footer a:eq(0)'); + click(".modal-footer a:eq(0)"); andThen(() => { - assert.equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); + assert.equal( + find(".d-editor-input") + .val() + .indexOf("This is the first post."), + 0, + "it populates the input with the post text" + ); }); }); @@ -348,26 +474,41 @@ QUnit.test("Disable body until category is selected", assert => { replaceCurrentUser({ admin: false, staff: false, trust_level: 1 }); visit("/"); - click('#create-topic'); + click("#create-topic"); andThen(() => { - assert.ok(exists('.d-editor-input'), 'the composer input is visible'); - assert.ok(exists('.title-input .popup-tip.bad.hide'), 'title errors are hidden by default'); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.bad.hide'), 'body errors are hidden by default'); - assert.ok(exists('.d-editor-textarea-wrapper.disabled'), 'textarea is disabled'); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); + assert.ok( + exists(".title-input .popup-tip.bad.hide"), + "title errors are hidden by default" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.bad.hide"), + "body errors are hidden by default" + ); + assert.ok( + exists(".d-editor-textarea-wrapper.disabled"), + "textarea is disabled" + ); }); - const categoryChooser = selectKit('.category-chooser'); + const categoryChooser = selectKit(".category-chooser"); categoryChooser.expand().selectRowByValue(2); andThen(() => { - assert.ok(find('.d-editor-textarea-wrapper.disabled').length === 0, 'textarea is enabled'); + assert.ok( + find(".d-editor-textarea-wrapper.disabled").length === 0, + "textarea is enabled" + ); }); - fillIn('.d-editor-input', 'Now I can type stuff'); - categoryChooser.expand().selectRowByValue('__none__'); + fillIn(".d-editor-input", "Now I can type stuff"); + categoryChooser.expand().selectRowByValue("__none__"); andThen(() => { - assert.ok(find('.d-editor-textarea-wrapper.disabled').length === 0, 'textarea is still enabled'); + assert.ok( + find(".d-editor-textarea-wrapper.disabled").length === 0, + "textarea is still enabled" + ); }); }); diff --git a/test/javascripts/acceptance/composer-topic-links-test.js.es6 b/test/javascripts/acceptance/composer-topic-links-test.js.es6 index 60bd14487e..5e2d128ed5 100644 --- a/test/javascripts/acceptance/composer-topic-links-test.js.es6 +++ b/test/javascripts/acceptance/composer-topic-links-test.js.es6 @@ -5,75 +5,155 @@ acceptance("Composer topic featured links", { settings: { topic_featured_link_enabled: true, max_topic_title_length: 80, - enable_markdown_linkify: true, + enable_markdown_linkify: true } }); - QUnit.test("onebox with title", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', "http://www.example.com/has-title.html"); + click("#create-topic"); + fillIn("#reply-title", "http://www.example.com/has-title.html"); andThen(() => { - assert.ok(find('.d-editor-preview').html().trim().indexOf('onebox') > 0, "it pastes the link into the body and previews it"); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good'); - assert.equal(find('.title-input input').val(), "An interesting article", "title is from the oneboxed article"); + assert.ok( + find(".d-editor-preview") + .html() + .trim() + .indexOf("onebox") > 0, + "it pastes the link into the body and previews it" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.good"), + "the body is now good" + ); + assert.equal( + find(".title-input input").val(), + "An interesting article", + "title is from the oneboxed article" + ); }); }); QUnit.test("onebox result doesn't include a title", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', 'http://www.example.com/no-title.html'); + click("#create-topic"); + fillIn("#reply-title", "http://www.example.com/no-title.html"); andThen(() => { - assert.ok(find('.d-editor-preview').html().trim().indexOf('onebox') > 0, "it pastes the link into the body and previews it"); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good'); - assert.equal(find('.title-input input').val(), "http://www.example.com/no-title.html", "title is unchanged"); + assert.ok( + find(".d-editor-preview") + .html() + .trim() + .indexOf("onebox") > 0, + "it pastes the link into the body and previews it" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.good"), + "the body is now good" + ); + assert.equal( + find(".title-input input").val(), + "http://www.example.com/no-title.html", + "title is unchanged" + ); }); }); QUnit.test("no onebox result", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', "http://www.example.com/nope-onebox.html"); + click("#create-topic"); + fillIn("#reply-title", "http://www.example.com/nope-onebox.html"); andThen(() => { - assert.ok(find('.d-editor-preview').html().trim().indexOf('onebox') > 0, "it pastes the link into the body and previews it"); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'link is pasted into body'); - assert.equal(find('.title-input input').val(), "http://www.example.com/nope-onebox.html", "title is unchanged"); + assert.ok( + find(".d-editor-preview") + .html() + .trim() + .indexOf("onebox") > 0, + "it pastes the link into the body and previews it" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.good"), + "link is pasted into body" + ); + assert.equal( + find(".title-input input").val(), + "http://www.example.com/nope-onebox.html", + "title is unchanged" + ); }); }); QUnit.test("ignore internal links", assert => { visit("/"); - click('#create-topic'); + click("#create-topic"); const title = "http://" + window.location.hostname + "/internal-page.html"; - fillIn('#reply-title', title); + fillIn("#reply-title", title); andThen(() => { - assert.equal(find('.d-editor-preview').html().trim().indexOf('onebox'), -1, "onebox preview doesn't show"); - assert.equal(find('.d-editor-input').val().length, 0, "link isn't put into the post"); - assert.equal(find('.title-input input').val(), title, "title is unchanged"); + assert.equal( + find(".d-editor-preview") + .html() + .trim() + .indexOf("onebox"), + -1, + "onebox preview doesn't show" + ); + assert.equal( + find(".d-editor-input").val().length, + 0, + "link isn't put into the post" + ); + assert.equal(find(".title-input input").val(), title, "title is unchanged"); }); }); QUnit.test("link is longer than max title length", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', "http://www.example.com/has-title-and-a-url-that-is-more-than-80-characters-because-thats-good-for-seo-i-guess.html"); + click("#create-topic"); + fillIn( + "#reply-title", + "http://www.example.com/has-title-and-a-url-that-is-more-than-80-characters-because-thats-good-for-seo-i-guess.html" + ); andThen(() => { - assert.ok(find('.d-editor-preview').html().trim().indexOf('onebox') > 0, "it pastes the link into the body and previews it"); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good'); - assert.equal(find('.title-input input').val(), "An interesting article", "title is from the oneboxed article"); + assert.ok( + find(".d-editor-preview") + .html() + .trim() + .indexOf("onebox") > 0, + "it pastes the link into the body and previews it" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.good"), + "the body is now good" + ); + assert.equal( + find(".title-input input").val(), + "An interesting article", + "title is from the oneboxed article" + ); }); }); QUnit.test("onebox with title but extra words in title field", assert => { visit("/"); - click('#create-topic'); - fillIn('#reply-title', "http://www.example.com/has-title.html test"); + click("#create-topic"); + fillIn("#reply-title", "http://www.example.com/has-title.html test"); andThen(() => { - assert.equal(find('.d-editor-preview').html().trim().indexOf('onebox'), -1, "onebox preview doesn't show"); - assert.equal(find('.d-editor-input').val().length, 0, "link isn't put into the post"); - assert.equal(find('.title-input input').val(), "http://www.example.com/has-title.html test", "title is unchanged"); + assert.equal( + find(".d-editor-preview") + .html() + .trim() + .indexOf("onebox"), + -1, + "onebox preview doesn't show" + ); + assert.equal( + find(".d-editor-input").val().length, + 0, + "link isn't put into the post" + ); + assert.equal( + find(".title-input input").val(), + "http://www.example.com/has-title.html test", + "title is unchanged" + ); }); }); @@ -91,15 +171,34 @@ QUnit.test("Pasting a link enables the text input area", assert => { replaceCurrentUser({ admin: false, staff: false, trust_level: 1 }); visit("/"); - click('#create-topic'); + click("#create-topic"); andThen(() => { - assert.ok(find('.d-editor-textarea-wrapper.disabled').length, 'textarea is disabled'); + assert.ok( + find(".d-editor-textarea-wrapper.disabled").length, + "textarea is disabled" + ); }); - fillIn('#reply-title', "http://www.example.com/has-title.html"); + fillIn("#reply-title", "http://www.example.com/has-title.html"); andThen(() => { - assert.ok(find('.d-editor-preview').html().trim().indexOf('onebox') > 0, "it pastes the link into the body and previews it"); - assert.ok(exists('.d-editor-textarea-wrapper .popup-tip.good'), 'the body is now good'); - assert.equal(find('.title-input input').val(), "An interesting article", "title is from the oneboxed article"); - assert.ok(find('.d-editor-textarea-wrapper.disabled').length === 0, 'textarea is enabled'); + assert.ok( + find(".d-editor-preview") + .html() + .trim() + .indexOf("onebox") > 0, + "it pastes the link into the body and previews it" + ); + assert.ok( + exists(".d-editor-textarea-wrapper .popup-tip.good"), + "the body is now good" + ); + assert.equal( + find(".title-input input").val(), + "An interesting article", + "title is from the oneboxed article" + ); + assert.ok( + find(".d-editor-textarea-wrapper.disabled").length === 0, + "textarea is enabled" + ); }); }); diff --git a/test/javascripts/acceptance/create-account-user-fields-test.js.es6 b/test/javascripts/acceptance/create-account-user-fields-test.js.es6 index cb7a2fe5c5..12a5fcb88b 100644 --- a/test/javascripts/acceptance/create-account-user-fields-test.js.es6 +++ b/test/javascripts/acceptance/create-account-user-fields-test.js.es6 @@ -2,9 +2,26 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("Create Account - User Fields", { site: { - user_fields: [{"id":34,"name":"I've read the terms of service","field_type":"confirm","required":true}, - {"id":35,"name":"What is your pet's name?","field_type":"text","required":true}, - {"id":36,"name":"What's your dad like?","field_type":"text","required":false}] + user_fields: [ + { + id: 34, + name: "I've read the terms of service", + field_type: "confirm", + required: true + }, + { + id: 35, + name: "What is your pet's name?", + field_type: "text", + required: true + }, + { + id: 36, + name: "What's your dad like?", + field_type: "text", + required: false + } + ] } }); @@ -13,35 +30,52 @@ QUnit.test("create account with user fields", assert => { click("header .sign-up-button"); andThen(() => { - assert.ok(exists('.create-account'), "it shows the create account modal"); - assert.ok(exists('.user-field'), "it has at least one user field"); - assert.ok(exists('.modal-footer .btn-primary:disabled'), 'create account is disabled at first'); + assert.ok(exists(".create-account"), "it shows the create account modal"); + assert.ok(exists(".user-field"), "it has at least one user field"); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "create account is disabled at first" + ); }); - fillIn('#new-account-name', 'Dr. Good Tuna'); - fillIn('#new-account-password', 'cool password bro'); - fillIn('#new-account-email', 'good.tuna@test.com'); - fillIn('#new-account-username', 'goodtuna'); + fillIn("#new-account-name", "Dr. Good Tuna"); + fillIn("#new-account-password", "cool password bro"); + fillIn("#new-account-email", "good.tuna@test.com"); + fillIn("#new-account-username", "goodtuna"); andThen(() => { - assert.ok(exists('#username-validation.good'), 'the username validation is good'); - assert.ok(exists('.modal-footer .btn-primary:disabled'), 'create account is still disabled due to lack of user fields'); + assert.ok( + exists("#username-validation.good"), + "the username validation is good" + ); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "create account is still disabled due to lack of user fields" + ); }); fillIn(".user-field input[type=text]:first", "Barky"); andThen(() => { - assert.ok(exists('.modal-footer .btn-primary:disabled'), 'create account is disabled because field is not checked'); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "create account is disabled because field is not checked" + ); }); click(".user-field input[type=checkbox]"); andThen(() => { - assert.not(exists('.modal-footer .btn-primary:disabled'), 'create account is enabled because field is not checked'); + assert.not( + exists(".modal-footer .btn-primary:disabled"), + "create account is enabled because field is not checked" + ); }); click(".user-field input[type=checkbox]"); andThen(() => { - assert.ok(exists('.modal-footer .btn-primary:disabled'), 'unclicking the checkbox disables the submit'); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "unclicking the checkbox disables the submit" + ); }); - }); diff --git a/test/javascripts/acceptance/custom-html-set-test.js.es6 b/test/javascripts/acceptance/custom-html-set-test.js.es6 index 6b2508ebf7..918b22ed87 100644 --- a/test/javascripts/acceptance/custom-html-set-test.js.es6 +++ b/test/javascripts/acceptance/custom-html-set-test.js.es6 @@ -1,30 +1,40 @@ import { acceptance } from "helpers/qunit-helpers"; -import { setCustomHTML } from 'discourse/helpers/custom-html'; -import PreloadStore from 'preload-store'; +import { setCustomHTML } from "discourse/helpers/custom-html"; +import PreloadStore from "preload-store"; acceptance("CustomHTML set"); QUnit.test("has no custom HTML in the top", assert => { visit("/static/faq"); andThen(() => { - assert.ok(!exists('span.custom-html-test'), 'it has no markup'); + assert.ok(!exists("span.custom-html-test"), "it has no markup"); }); }); QUnit.test("renders set HTML", assert => { - setCustomHTML('top', 'HTML'); + setCustomHTML("top", 'HTML'); visit("/static/faq"); andThen(() => { - assert.equal(find('span.custom-html-test').text(), 'HTML', 'it inserted the markup'); + assert.equal( + find("span.custom-html-test").text(), + "HTML", + "it inserted the markup" + ); }); }); QUnit.test("renders preloaded HTML", assert => { - PreloadStore.store('customHTML', {top: "monster"}); + PreloadStore.store("customHTML", { + top: "monster" + }); visit("/static/faq"); andThen(() => { - assert.equal(find('span.cookie').text(), 'monster', 'it inserted the markup'); + assert.equal( + find("span.cookie").text(), + "monster", + "it inserted the markup" + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/custom-html-template-test.js.es6 b/test/javascripts/acceptance/custom-html-template-test.js.es6 index bb0d77c879..ab86bc86d6 100644 --- a/test/javascripts/acceptance/custom-html-template-test.js.es6 +++ b/test/javascripts/acceptance/custom-html-template-test.js.es6 @@ -2,17 +2,23 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("CustomHTML template", { beforeEach() { - Ember.TEMPLATES['top'] = Ember.HTMLBars.compile(`TOP`); + Ember.TEMPLATES["top"] = Ember.HTMLBars.compile( + `TOP` + ); }, afterEach() { - delete Ember.TEMPLATES['top']; + delete Ember.TEMPLATES["top"]; } }); QUnit.test("renders custom template", assert => { visit("/static/faq"); andThen(() => { - assert.equal(find('span.top-span').text(), 'TOP', 'it inserted the template'); + assert.equal( + find("span.top-span").text(), + "TOP", + "it inserted the template" + ); }); }); diff --git a/test/javascripts/acceptance/dashboard-next-test.js.es6 b/test/javascripts/acceptance/dashboard-next-test.js.es6 index 46d50d0087..56aef0402b 100644 --- a/test/javascripts/acceptance/dashboard-next-test.js.es6 +++ b/test/javascripts/acceptance/dashboard-next-test.js.es6 @@ -1,7 +1,4 @@ -import { - acceptance -} -from "helpers/qunit-helpers"; +import { acceptance } from "helpers/qunit-helpers"; acceptance("Dashboard Next", { loggedIn: true @@ -11,12 +8,33 @@ QUnit.test("Visit dashboard next page", assert => { visit("/admin"); andThen(() => { - assert.ok($('.dashboard-next').length, "has dashboard-next class"); + assert.ok($(".dashboard-next").length, "has dashboard-next class"); - assert.ok($('.dashboard-mini-chart.signups').length, "has a signups chart"); - assert.ok($('.dashboard-mini-chart.posts').length, "has a posts chart"); - assert.ok($('.dashboard-mini-chart.dau_by_mau').length, "has a dau_by_mau chart"); - assert.ok($('.dashboard-mini-chart.daily_engaged_users').length, "has a daily_engaged_users chart"); - assert.ok($('.dashboard-mini-chart.new_contributors').length, "has a new_contributors chart"); + assert.ok($(".dashboard-mini-chart.signups").length, "has a signups chart"); + + assert.ok($(".dashboard-mini-chart.posts").length, "has a posts chart"); + + assert.ok( + $(".dashboard-mini-chart.dau_by_mau").length, + "has a dau_by_mau chart" + ); + + assert.ok( + $(".dashboard-mini-chart.daily_engaged_users").length, + "has a daily_engaged_users chart" + ); + + assert.ok( + $(".dashboard-mini-chart.new_contributors").length, + "has a new_contributors chart" + ); + + assert.equal( + $(".section.dashboard-problems .problem-messages ul li:first-child") + .html() + .trim(), + "Houston...", + "displays problems" + ); }); }); diff --git a/test/javascripts/acceptance/emoji-picker-test.js.es6 b/test/javascripts/acceptance/emoji-picker-test.js.es6 index 56a0f2b926..2933e55381 100644 --- a/test/javascripts/acceptance/emoji-picker-test.js.es6 +++ b/test/javascripts/acceptance/emoji-picker-test.js.es6 @@ -1,10 +1,12 @@ import { acceptance } from "helpers/qunit-helpers"; -import { IMAGE_VERSION as v } from 'pretty-text/emoji'; -import { resetCache } from 'discourse/components/emoji-picker'; +import { IMAGE_VERSION as v } from "pretty-text/emoji"; +import { resetCache } from "discourse/components/emoji-picker"; acceptance("EmojiPicker", { loggedIn: true, - beforeEach() { resetCache(); } + beforeEach() { + resetCache(); + } }); QUnit.test("emoji picker can be opened/closed", assert => { @@ -14,7 +16,9 @@ QUnit.test("emoji picker can be opened/closed", assert => { click("button.emoji.btn"); andThen(() => { assert.notEqual( - find('.emoji-picker').html().trim(), + find(".emoji-picker") + .html() + .trim(), "", "it opens the picker" ); @@ -23,10 +27,12 @@ QUnit.test("emoji picker can be opened/closed", assert => { click("button.emoji.btn"); andThen(() => { assert.equal( - find('.emoji-picker').html().trim(), + find(".emoji-picker") + .html() + .trim(), "", "it closes the picker" - ); + ); }); }); @@ -36,10 +42,12 @@ QUnit.test("emojis can be hovered to display info", assert => { click("button.emoji.btn"); andThen(() => { - $(".emoji-picker button[title='grinning']").trigger('mouseover'); + $(".emoji-picker button[title='grinning']").trigger("mouseover"); andThen(() => { assert.equal( - find('.emoji-picker .info').html().trim(), + find(".emoji-picker .info") + .html() + .trim(), ` :grinning:`, "it displays emoji info when hovering emoji" ); @@ -55,7 +63,7 @@ QUnit.test("emoji picker triggers event when picking emoji", assert => { click(".emoji-picker button[title='grinning']"); andThen(() => { assert.equal( - find('.d-editor-input').val(), + find(".d-editor-input").val(), ":grinning:", "it adds the emoji code in the editor when selected" ); @@ -67,7 +75,9 @@ QUnit.test("emoji picker has a list of recently used emojis", assert => { click("#topic-footer-buttons .btn.create"); click("button.emoji.btn"); - click(".emoji-picker .section[data-section='people'] button.emoji[title='grinning']"); + click( + ".emoji-picker .section[data-section='people'] button.emoji[title='grinning']" + ); andThen(() => { assert.equal( find('.emoji-picker .section[data-section="recent"]').css("display"), @@ -76,7 +86,9 @@ QUnit.test("emoji picker has a list of recently used emojis", assert => { ); assert.equal( - find('.emoji-picker .section[data-section="recent"] .section-group button.emoji').length, + find( + '.emoji-picker .section[data-section="recent"] .section-group button.emoji' + ).length, 1, "it adds the emoji code to the recently used emojis list" ); @@ -85,7 +97,9 @@ QUnit.test("emoji picker has a list of recently used emojis", assert => { click(".emoji-picker .clear-recent"); andThen(() => { assert.equal( - find('.emoji-picker .section[data-section="recent"] .section-group button.emoji').length, + find( + '.emoji-picker .section[data-section="recent"] .section-group button.emoji' + ).length, 0, "it has cleared recent emojis" ); @@ -97,7 +111,9 @@ QUnit.test("emoji picker has a list of recently used emojis", assert => { ); assert.equal( - find('.emoji-picker .category-icon button.emoji[data-section="recent"]').parent().css("display"), + find('.emoji-picker .category-icon button.emoji[data-section="recent"]') + .parent() + .css("display"), "none", "it hides recent category icon" ); @@ -114,13 +130,18 @@ QUnit.test("emoji picker correctly orders recently used emojis", assert => { click(".emoji-picker button[title='grinning']"); andThen(() => { assert.equal( - find('.section[data-section="recent"] .section-group button.emoji').length, + find('.section[data-section="recent"] .section-group button.emoji') + .length, 2, "it has multiple recent emojis" ); assert.equal( - /grinning/.test(find('.section[data-section="recent"] .section-group button.emoji').first().css('background-image')), + /grinning/.test( + find('.section[data-section="recent"] .section-group button.emoji') + .first() + .css("background-image") + ), true, "it puts the last used emoji in first" ); @@ -136,28 +157,29 @@ QUnit.test("emoji picker lazy loads emojis", assert => { andThen(() => { assert.equal( - find('.emoji-picker button[title="massage_woman"]').css("background-image"), + find('.emoji-picker button[title="massage_woman"]').css( + "background-image" + ), "none", "it doesn't load invisible emojis" ); }); }); - QUnit.test("emoji picker persists state", assert => { visit("/t/internationalization-localization/280"); click("#topic-footer-buttons .btn.create"); click("button.emoji.btn"); andThen(() => { - click('.emoji-picker a.diversity-scale.medium-dark'); + click(".emoji-picker a.diversity-scale.medium-dark"); }); click("button.emoji.btn"); click("button.emoji.btn"); andThen(() => { assert.equal( - find('.emoji-picker .diversity-scale.medium-dark').hasClass('selected'), + find(".emoji-picker .diversity-scale.medium-dark").hasClass("selected"), true, "it stores diversity scale" ); diff --git a/test/javascripts/acceptance/emoji-test.js.es6 b/test/javascripts/acceptance/emoji-test.js.es6 index 23fc2aa804..b6d46df09b 100644 --- a/test/javascripts/acceptance/emoji-test.js.es6 +++ b/test/javascripts/acceptance/emoji-test.js.es6 @@ -4,30 +4,50 @@ acceptance("Emoji", { loggedIn: true }); QUnit.test("emoji is cooked properly", assert => { visit("/t/internationalization-localization/280"); - click('#topic-footer-buttons .btn.create'); + click("#topic-footer-buttons .btn.create"); - fillIn('.d-editor-input', "this is an emoji :blonde_woman:"); + fillIn(".d-editor-input", "this is an emoji :blonde_woman:"); andThen(() => { - assert.equal(find('.d-editor-preview:visible').html().trim(), "

this is an emoji \":blonde_woman:\"

"); + assert.equal( + find(".d-editor-preview:visible") + .html() + .trim(), + '

this is an emoji :blonde_woman:

' + ); }); - click('#reply-control .btn.create'); + click("#reply-control .btn.create"); andThen(() => { - assert.equal(find('.topic-post:last .cooked p').html().trim(), "this is an emoji \":blonde_woman:\""); + assert.equal( + find(".topic-post:last .cooked p") + .html() + .trim(), + 'this is an emoji :blonde_woman:' + ); }); }); QUnit.test("skin toned emoji is cooked properly", assert => { visit("/t/internationalization-localization/280"); - click('#topic-footer-buttons .btn.create'); + click("#topic-footer-buttons .btn.create"); - fillIn('.d-editor-input', "this is an emoji :blonde_woman:t5:"); + fillIn(".d-editor-input", "this is an emoji :blonde_woman:t5:"); andThen(() => { - assert.equal(find('.d-editor-preview:visible').html().trim(), "

this is an emoji \":blonde_woman:t5:\"

"); + assert.equal( + find(".d-editor-preview:visible") + .html() + .trim(), + '

this is an emoji :blonde_woman:t5:

' + ); }); - click('#reply-control .btn.create'); + click("#reply-control .btn.create"); andThen(() => { - assert.equal(find('.topic-post:last .cooked p').html().trim(), "this is an emoji \":blonde_woman:t5:\""); + assert.equal( + find(".topic-post:last .cooked p") + .html() + .trim(), + 'this is an emoji :blonde_woman:t5:' + ); }); }); diff --git a/test/javascripts/acceptance/forgot-password-test.js.es6 b/test/javascripts/acceptance/forgot-password-test.js.es6 index 19a0d39c6b..2107bbacf2 100644 --- a/test/javascripts/acceptance/forgot-password-test.js.es6 +++ b/test/javascripts/acceptance/forgot-password-test.js.es6 @@ -5,15 +5,12 @@ let userFound = false; acceptance("Forgot password", { beforeEach() { const response = object => { - return [ - 200, - { "Content-Type": "application/json" }, - object - ]; + return [200, { "Content-Type": "application/json" }, object]; }; - server.post('/session/forgot_password', () => { // eslint-disable-line no-undef - return response({ "user_found": userFound }); + // prettier-ignore + server.post("/session/forgot_password", () => { // eslint-disable-line no-undef + return response({ user_found: userFound }); }); } }); @@ -21,67 +18,86 @@ acceptance("Forgot password", { QUnit.test("requesting password reset", assert => { visit("/"); click("header .login-button"); - click('#forgot-password-link'); + click("#forgot-password-link"); andThen(() => { assert.equal( - find('.forgot-password-reset').attr("disabled"), + find(".forgot-password-reset").attr("disabled"), "disabled", - 'it should disable the button until the field is filled' + "it should disable the button until the field is filled" ); }); - fillIn("#username-or-email", 'someuser'); - click('.forgot-password-reset'); + fillIn("#username-or-email", "someuser"); + click(".forgot-password-reset"); andThen(() => { assert.equal( - find(".alert-error").html().trim(), - I18n.t('forgot_password.complete_username_not_found', { username: 'someuser' }), - 'it should display an error for an invalid username' + find(".alert-error") + .html() + .trim(), + I18n.t("forgot_password.complete_username_not_found", { + username: "someuser" + }), + "it should display an error for an invalid username" ); }); - fillIn("#username-or-email", 'someuser@gmail.com'); - click('.forgot-password-reset'); + fillIn("#username-or-email", "someuser@gmail.com"); + click(".forgot-password-reset"); andThen(() => { assert.equal( - find(".alert-error").html().trim(), - I18n.t('forgot_password.complete_email_not_found', { email: 'someuser@gmail.com' }), - 'it should display an error for an invalid email' + find(".alert-error") + .html() + .trim(), + I18n.t("forgot_password.complete_email_not_found", { + email: "someuser@gmail.com" + }), + "it should display an error for an invalid email" ); }); - fillIn("#username-or-email", 'someuser'); + fillIn("#username-or-email", "someuser"); andThen(() => { userFound = true; }); - click('.forgot-password-reset'); + click(".forgot-password-reset"); andThen(() => { - assert.notOk(exists(find(".alert-error")), 'it should remove the flash error when succeeding'); + assert.notOk( + exists(find(".alert-error")), + "it should remove the flash error when succeeding" + ); assert.equal( - find(".modal-body").html().trim(), - I18n.t('forgot_password.complete_username_found', { username: 'someuser' }), - 'it should display a success message for a valid username' + find(".modal-body") + .html() + .trim(), + I18n.t("forgot_password.complete_username_found", { + username: "someuser" + }), + "it should display a success message for a valid username" ); }); visit("/"); click("header .login-button"); - click('#forgot-password-link'); - fillIn("#username-or-email", 'someuser@gmail.com'); - click('.forgot-password-reset'); + click("#forgot-password-link"); + fillIn("#username-or-email", "someuser@gmail.com"); + click(".forgot-password-reset"); andThen(() => { assert.equal( - find(".modal-body").html().trim(), - I18n.t('forgot_password.complete_email_found', { email: 'someuser@gmail.com' }), - 'it should display a success message for a valid email' + find(".modal-body") + .html() + .trim(), + I18n.t("forgot_password.complete_email_found", { + email: "someuser@gmail.com" + }), + "it should display a success message for a valid email" ); }); }); diff --git a/test/javascripts/acceptance/group-card-mobile-test.js.es6 b/test/javascripts/acceptance/group-card-mobile-test.js.es6 index 33204c8962..d3b6fcc9b0 100644 --- a/test/javascripts/acceptance/group-card-mobile-test.js.es6 +++ b/test/javascripts/acceptance/group-card-mobile-test.js.es6 @@ -3,13 +3,12 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("Group Card - Mobile", { mobileView: true }); QUnit.test("group card", assert => { - visit('/t/301/1'); + visit("/t/301/1"); - assert.ok(invisible('#group-card'), 'user card is invisible by default'); - click('a.mention-group:first'); + assert.ok(invisible("#group-card"), "user card is invisible by default"); + click("a.mention-group:first"); andThen(() => { - assert.ok(visible('.group-details-container'), 'group page show be shown'); + assert.ok(visible(".group-details-container"), "group page show be shown"); }); - }); diff --git a/test/javascripts/acceptance/group-card-test.js.es6 b/test/javascripts/acceptance/group-card-test.js.es6 index 15389afd12..5ca3f4ad77 100644 --- a/test/javascripts/acceptance/group-card-test.js.es6 +++ b/test/javascripts/acceptance/group-card-test.js.es6 @@ -3,13 +3,12 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("Group Card"); QUnit.test("group card", assert => { - visit('/t/301/1'); + visit("/t/301/1"); - assert.ok(invisible('#group-card'), 'user card is invisible by default'); - click('a.mention-group:first'); + assert.ok(invisible("#group-card"), "user card is invisible by default"); + click("a.mention-group:first"); andThen(() => { - assert.ok(visible('#group-card'), 'card should appear'); + assert.ok(visible("#group-card"), "card should appear"); }); - }); diff --git a/test/javascripts/acceptance/group-index-test.js.es6 b/test/javascripts/acceptance/group-index-test.js.es6 index dbb768639c..bd6dc23e3e 100644 --- a/test/javascripts/acceptance/group-index-test.js.es6 +++ b/test/javascripts/acceptance/group-index-test.js.es6 @@ -6,18 +6,21 @@ QUnit.test("Viewing Members as anon user", assert => { visit("/groups/discourse"); andThen(() => { - assert.ok(count('.avatar-flair .fa-adjust') === 1, "it displays the group's avatar flair"); - assert.ok(count('.group-members tr') > 0, "it lists group members"); + assert.ok( + count(".avatar-flair .fa-adjust") === 1, + "it displays the group's avatar flair" + ); + assert.ok(count(".group-members tr") > 0, "it lists group members"); assert.ok( - count('.group-member-dropdown') === 0, - 'it does not allow anon user to manage group members' + count(".group-member-dropdown") === 0, + "it does not allow anon user to manage group members" ); assert.equal( - find('.group-username-filter').attr('placeholder'), - I18n.t('groups.members.filter_placeholder'), - 'it should display the right filter placehodler' + find(".group-username-filter").attr("placeholder"), + I18n.t("groups.members.filter_placeholder"), + "it should display the right filter placehodler" ); }); }); @@ -28,12 +31,13 @@ QUnit.test("Viewing Members as a group owner", assert => { replaceCurrentUser({ admin: false, staff: false }); visit("/groups/discourse"); - click('.group-members-add'); + click(".group-members-add"); andThen(() => { assert.equal( - find('#group-add-members-user-selector').length, 1, - 'it should display the add members modal' + find("#group-add-members-user-selector").length, + 1, + "it should display the add members modal" ); }); }); @@ -46,14 +50,14 @@ QUnit.test("Viewing Members as an admin user", assert => { andThen(() => { assert.ok( - count('.group-member-dropdown') > 0, - 'it allows admin user to manage group members' + count(".group-member-dropdown") > 0, + "it allows admin user to manage group members" ); assert.equal( - find('.group-username-filter').attr('placeholder'), - I18n.t('groups.members.filter_placeholder_admin'), - 'it should display the right filter placehodler' + find(".group-username-filter").attr("placeholder"), + I18n.t("groups.members.filter_placeholder_admin"), + "it should display the right filter placehodler" ); }); }); diff --git a/test/javascripts/acceptance/group-manage-interaction-test.js.es6 b/test/javascripts/acceptance/group-manage-interaction-test.js.es6 index 59793c1a8e..06a1c913cb 100644 --- a/test/javascripts/acceptance/group-manage-interaction-test.js.es6 +++ b/test/javascripts/acceptance/group-manage-interaction-test.js.es6 @@ -12,28 +12,33 @@ QUnit.test("As an admin", assert => { andThen(() => { assert.equal( - find('.groups-form-visibility-level').length, 1, - 'it should display visibility level selector' + find(".groups-form-visibility-level").length, + 1, + "it should display visibility level selector" ); assert.equal( - find('.groups-form-mentionable-level').length, 1, - 'it should display mentionable level selector' + find(".groups-form-mentionable-level").length, + 1, + "it should display mentionable level selector" ); assert.equal( - find('.groups-form-messageable-level').length, 1, - 'it should display messageable level selector' + find(".groups-form-messageable-level").length, + 1, + "it should display messageable level selector" ); assert.equal( - find('.groups-form-incoming-email').length, 1, - 'it should display incoming email input' + find(".groups-form-incoming-email").length, + 1, + "it should display incoming email input" ); assert.equal( - find('.groups-form-default-notification-level').length, 1, - 'it should display default notification level input' + find(".groups-form-default-notification-level").length, + 1, + "it should display default notification level input" ); }); }); @@ -44,28 +49,33 @@ QUnit.test("As a group owner", assert => { andThen(() => { assert.equal( - find('.groups-form-visibility-level').length, 0, - 'it should display visibility level selector' + find(".groups-form-visibility-level").length, + 0, + "it should display visibility level selector" ); assert.equal( - find('.groups-form-mentionable-level').length, 1, - 'it should display mentionable level selector' + find(".groups-form-mentionable-level").length, + 1, + "it should display mentionable level selector" ); assert.equal( - find('.groups-form-messageable-level').length, 1, - 'it should display messageable level selector' + find(".groups-form-messageable-level").length, + 1, + "it should display messageable level selector" ); assert.equal( - find('.groups-form-incoming-email').length, 0, - 'it should not display incoming email input' + find(".groups-form-incoming-email").length, + 0, + "it should not display incoming email input" ); assert.equal( - find('.groups-form-default-notification-level').length, 1, - 'it should display default notification level input' + find(".groups-form-default-notification-level").length, + 1, + "it should display default notification level input" ); }); }); diff --git a/test/javascripts/acceptance/group-manage-logs-test.js.es6 b/test/javascripts/acceptance/group-manage-logs-test.js.es6 index 95c76dc70a..a600e489b5 100644 --- a/test/javascripts/acceptance/group-manage-logs-test.js.es6 +++ b/test/javascripts/acceptance/group-manage-logs-test.js.es6 @@ -4,23 +4,96 @@ acceptance("Group logs", { loggedIn: true, beforeEach() { const response = object => { - return [ - 200, - { "Content-Type": "application/json" }, - object - ]; + return [200, { "Content-Type": "application/json" }, object]; }; - server.get('/groups/snorlax.json', () => { // eslint-disable-line no-undef - return response({"group":{"id":41,"automatic":false,"name":"snorlax","user_count":1,"alias_level":0,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":true,"title":"Team Snorlax","grant_trust_level":null,"incoming_email":null,"has_messages":false,"flair_url":"","flair_bg_color":"","flair_color":"","bio_raw":"","bio_cooked":null,"public":true,"is_group_user":true,"is_group_owner":true}}); + // prettier-ignore + server.get("/groups/snorlax.json", () => { // eslint-disable-line no-undef + return response({ + group: { + id: 41, + automatic: false, + name: "snorlax", + user_count: 1, + alias_level: 0, + visible: true, + automatic_membership_email_domains: "", + automatic_membership_retroactive: false, + primary_group: true, + title: "Team Snorlax", + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: "", + flair_bg_color: "", + flair_color: "", + bio_raw: "", + bio_cooked: null, + public: true, + is_group_user: true, + is_group_owner: true + } + }); }); // Workaround while awaiting https://github.com/tildeio/route-recognizer/issues/53 - server.get('/groups/snorlax/logs.json', request => { // eslint-disable-line no-undef + // prettier-ignore + server.get("/groups/snorlax/logs.json", request => { // eslint-disable-line no-undef if (request.queryParams["filters[action]"]) { - return response({"logs":[{"action":"change_group_setting","subject":"title","prev_value":null,"new_value":"Team Snorlax","created_at":"2016-12-12T08:27:46.408Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":null}],"all_loaded":true}); + return response({ + logs: [ + { + action: "change_group_setting", + subject: "title", + prev_value: null, + new_value: "Team Snorlax", + created_at: "2016-12-12T08:27:46.408Z", + acting_user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + target_user: null + } + ], + all_loaded: true + }); } else { - return response({"logs":[{"action":"change_group_setting","subject":"title","prev_value":null,"new_value":"Team Snorlax","created_at":"2016-12-12T08:27:46.408Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":null},{"action":"add_user_to_group","subject":null,"prev_value":null,"new_value":null,"created_at":"2016-12-12T08:27:27.725Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}],"all_loaded":true}); + return response({ + logs: [ + { + action: "change_group_setting", + subject: "title", + prev_value: null, + new_value: "Team Snorlax", + created_at: "2016-12-12T08:27:46.408Z", + acting_user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + target_user: null + }, + { + action: "add_user_to_group", + subject: null, + prev_value: null, + new_value: null, + created_at: "2016-12-12T08:27:27.725Z", + acting_user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + }, + target_user: { + id: 1, + username: "tgx", + avatar_template: "/images/avatar.png" + } + } + ], + all_loaded: true + }); } }); } @@ -30,11 +103,17 @@ QUnit.test("Browsing group logs", assert => { visit("/groups/snorlax/manage/logs"); andThen(() => { - assert.ok(find('tr.group-manage-logs-row').length === 2, 'it should display the right number of logs'); + assert.ok( + find("tr.group-manage-logs-row").length === 2, + "it should display the right number of logs" + ); click(find(".group-manage-logs-row button")[0]); }); andThen(() => { - assert.ok(find('tr.group-manage-logs-row').length === 1, 'it should display the right number of logs'); + assert.ok( + find("tr.group-manage-logs-row").length === 1, + "it should display the right number of logs" + ); }); }); diff --git a/test/javascripts/acceptance/group-manage-membership-test.js.es6 b/test/javascripts/acceptance/group-manage-membership-test.js.es6 index 5b1dad16e5..d22aa56bed 100644 --- a/test/javascripts/acceptance/group-manage-membership-test.js.es6 +++ b/test/javascripts/acceptance/group-manage-membership-test.js.es6 @@ -10,62 +10,63 @@ QUnit.test("As an admin", assert => { andThen(() => { assert.ok( find('label[for="automatic_membership"]').length === 1, - 'it should display automatic membership label' + "it should display automatic membership label" ); assert.ok( - find('.groups-form-automatic-membership-retroactive').length === 1, - 'it should display automatic membership retroactive checkbox' + find(".groups-form-automatic-membership-retroactive").length === 1, + "it should display automatic membership retroactive checkbox" ); assert.ok( - find('.groups-form-primary-group').length === 1, - 'it should display set as primary group checkbox' + find(".groups-form-primary-group").length === 1, + "it should display set as primary group checkbox" ); assert.ok( - find('.groups-form-grant-trust-level').length === 1, - 'it should display grant trust level selector' + find(".groups-form-grant-trust-level").length === 1, + "it should display grant trust level selector" ); assert.ok( - find('.group-form-public-admission').length === 1, - 'it should display group public admission input' + find(".group-form-public-admission").length === 1, + "it should display group public admission input" ); assert.ok( - find('.group-form-public-exit').length === 1, - 'it should display group public exit input' + find(".group-form-public-exit").length === 1, + "it should display group public exit input" ); assert.ok( - find('.group-form-allow-membership-requests').length === 1, - 'it should display group allow_membership_request input' + find(".group-form-allow-membership-requests").length === 1, + "it should display group allow_membership_request input" ); assert.ok( - find('.group-form-allow-membership-requests[disabled]').length === 1, - 'it should disable group allow_membership_request input' + find(".group-form-allow-membership-requests[disabled]").length === 1, + "it should disable group allow_membership_request input" ); }); - click('.group-form-public-admission'); - click('.group-form-allow-membership-requests'); + click(".group-form-public-admission"); + click(".group-form-allow-membership-requests"); andThen(() => { assert.ok( - find('.group-form-public-admission[disabled]').length === 1, - 'it should disable group public admission input' + find(".group-form-public-admission[disabled]").length === 1, + "it should disable group public admission input" ); assert.ok( - find('.group-form-public-exit[disabled]').length === 0, - 'it should not disable group public exit input' + find(".group-form-public-exit[disabled]").length === 0, + "it should not disable group public exit input" ); assert.equal( - find('.group-form-membership-request-template').length, 1, - 'it should display the membership request template field' + find(".group-form-membership-request-template").length, + 1, + "it should display the membership request template field" ); }); }); @@ -78,42 +79,42 @@ QUnit.test("As a group owner", assert => { andThen(() => { assert.ok( find('label[for="automatic_membership"]').length === 0, - 'it should not display automatic membership label' + "it should not display automatic membership label" ); assert.ok( - find('.groups-form-automatic-membership-retroactive').length === 0, - 'it should not display automatic membership retroactive checkbox' + find(".groups-form-automatic-membership-retroactive").length === 0, + "it should not display automatic membership retroactive checkbox" ); assert.ok( - find('.groups-form-primary-group').length === 0, - 'it should not display set as primary group checkbox' + find(".groups-form-primary-group").length === 0, + "it should not display set as primary group checkbox" ); assert.ok( - find('.groups-form-grant-trust-level').length === 0, - 'it should not display grant trust level selector' + find(".groups-form-grant-trust-level").length === 0, + "it should not display grant trust level selector" ); assert.ok( - find('.group-form-public-admission').length === 1, - 'it should display group public admission input' + find(".group-form-public-admission").length === 1, + "it should display group public admission input" ); assert.ok( - find('.group-form-public-exit').length === 1, - 'it should display group public exit input' + find(".group-form-public-exit").length === 1, + "it should display group public exit input" ); assert.ok( - find('.group-form-allow-membership-requests').length === 1, - 'it should display group allow_membership_request input' + find(".group-form-allow-membership-requests").length === 1, + "it should display group allow_membership_request input" ); assert.ok( - find('.group-form-allow-membership-requests[disabled]').length === 1, - 'it should disable group allow_membership_request input' + find(".group-form-allow-membership-requests[disabled]").length === 1, + "it should disable group allow_membership_request input" ); }); }); diff --git a/test/javascripts/acceptance/group-manage-profile-test.js.es6 b/test/javascripts/acceptance/group-manage-profile-test.js.es6 index f8e42d2972..351ebd084d 100644 --- a/test/javascripts/acceptance/group-manage-profile-test.js.es6 +++ b/test/javascripts/acceptance/group-manage-profile-test.js.es6 @@ -9,10 +9,22 @@ QUnit.test("As an admin", assert => { visit("/groups/discourse/manage/profile"); andThen(() => { - assert.ok(find('.group-flair-inputs').length === 1, 'it should display avatar flair inputs'); - assert.ok(find('.group-form-bio').length === 1, 'it should display group bio input'); - assert.ok(find('.group-form-name').length === 1, 'it should display group name input'); - assert.ok(find('.group-form-full-name').length === 1, 'it should display group full name input'); + assert.ok( + find(".group-flair-inputs").length === 1, + "it should display avatar flair inputs" + ); + assert.ok( + find(".group-form-bio").length === 1, + "it should display group bio input" + ); + assert.ok( + find(".group-form-name").length === 1, + "it should display group name input" + ); + assert.ok( + find(".group-form-full-name").length === 1, + "it should display group full name input" + ); }); }); @@ -25,8 +37,9 @@ QUnit.test("As a group owner", assert => { andThen(() => { assert.equal( - find('.group-form-name').length, 0, - 'it should not display group name input' + find(".group-form-name").length, + 0, + "it should not display group name input" ); }); }); @@ -35,6 +48,9 @@ QUnit.test("As an anonymous user", assert => { visit("/groups/discourse/manage/profile"); andThen(() => { - assert.ok(count('.group-members tr') > 0, "it should redirect to members page for an anonymous user"); + assert.ok( + count(".group-members tr") > 0, + "it should redirect to members page for an anonymous user" + ); }); }); diff --git a/test/javascripts/acceptance/group-test.js.es6 b/test/javascripts/acceptance/group-test.js.es6 index a119b5aa4c..7cbffeb72e 100644 --- a/test/javascripts/acceptance/group-test.js.es6 +++ b/test/javascripts/acceptance/group-test.js.es6 @@ -7,11 +7,7 @@ acceptance("Group", { }); const response = object => { - return [ - 200, - { "Content-Type": "application/json" }, - object - ]; + return [200, { "Content-Type": "application/json" }, object]; }; QUnit.test("Anonymous Viewing Group", async assert => { @@ -20,46 +16,59 @@ QUnit.test("Anonymous Viewing Group", async assert => { assert.equal( count(".nav-pills li a[title='Messages']"), 0, - 'it deos not show group messages navigation link' + "it deos not show group messages navigation link" ); await click(".nav-pills li a[title='Activity']"); - assert.ok(count('.user-stream-item') > 0, "it lists stream items"); + assert.ok(count(".user-stream-item") > 0, "it lists stream items"); await click(".activity-nav li a[href='/groups/discourse/activity/topics']"); - assert.ok(find('.topic-list'), "it shows the topic list"); - assert.equal(count('.topic-list-item'), 2, "it lists stream items"); + assert.ok(find(".topic-list"), "it shows the topic list"); + assert.equal(count(".topic-list-item"), 2, "it lists stream items"); await click(".activity-nav li a[href='/groups/discourse/activity/mentions']"); - assert.ok(count('.user-stream-item') > 0, "it lists stream items"); - assert.ok(find(".nav-pills li a[title='Edit Group']").length === 0, 'it should not show messages tab if user is not admin'); - assert.ok(find(".nav-pills li a[title='Logs']").length === 0, 'it should not show Logs tab if user is not admin'); - assert.ok(count('.user-stream-item') > 0, "it lists stream items"); + assert.ok(count(".user-stream-item") > 0, "it lists stream items"); + assert.ok( + find(".nav-pills li a[title='Edit Group']").length === 0, + "it should not show messages tab if user is not admin" + ); + assert.ok( + find(".nav-pills li a[title='Logs']").length === 0, + "it should not show Logs tab if user is not admin" + ); + assert.ok(count(".user-stream-item") > 0, "it lists stream items"); - await expandSelectKit('.group-dropdown'); + await expandSelectKit(".group-dropdown"); assert.equal( - find('.select-kit-row').text().trim(), 'discourse', - 'it displays the right row' + find(".select-kit-row") + .text() + .trim(), + "discourse", + "it displays the right row" ); assert.equal( - find('.group-dropdown-filter').text().trim(), I18n.t("groups.index.all").toLowerCase(), - 'it displays the right header' + find(".group-dropdown-filter") + .text() + .trim(), + I18n.t("groups.index.all").toLowerCase(), + "it displays the right header" ); Discourse.SiteSettings.enable_group_directory = false; await visit("/groups"); await visit("/groups/discourse"); - await expandSelectKit('.group-dropdown'); + await expandSelectKit(".group-dropdown"); assert.equal( - find('.group-dropdown-filter').length, 0, - 'it should not display the default header' + find(".group-dropdown-filter").length, + 0, + "it should not display the default header" ); }); @@ -70,7 +79,7 @@ QUnit.test("Anonymous Viewing Automatic Group", assert => { assert.equal( count(".nav-pills li a[title='Manage']"), 0, - 'it deos not show group messages navigation link' + "it deos not show group messages navigation link" ); }); }); @@ -80,67 +89,164 @@ QUnit.test("User Viewing Group", assert => { Discourse.reset(); visit("/groups"); - click('.group-index-request'); + click(".group-index-request"); - server.post('/groups/Macdonald/request_membership', () => { // eslint-disable-line no-undef + // prettier-ignore + server.post("/groups/Macdonald/request_membership", () => { // eslint-disable-line no-undef return [ 200, { "Content-Type": "application/json" }, - { relative_url: '/t/internationalization-localization/280' } + { relative_url: "/t/internationalization-localization/280" } ]; }); andThen(() => { - assert.equal(find('.modal-header').text().trim(), I18n.t( - 'groups.membership_request.title', { group_name: 'Macdonald' } - )); + assert.equal( + find(".modal-header") + .text() + .trim(), + I18n.t("groups.membership_request.title", { group_name: "Macdonald" }) + ); - assert.equal(find('.request-group-membership-form textarea').val(), 'Please add me'); + assert.equal( + find(".request-group-membership-form textarea").val(), + "Please add me" + ); }); - click('.modal-footer .btn-primary'); + click(".modal-footer .btn-primary"); andThen(() => { assert.equal( - find('.fancy-title').text().trim(), + find(".fancy-title") + .text() + .trim(), "Internationalization / localization" ); }); visit("/groups/discourse"); - click('.group-message-button'); - - andThen(() => { - assert.ok(count('#reply-control') === 1, 'it opens the composer'); - assert.equal(find('.ac-wrap .item').text(), 'discourse', 'it prefills the group name'); - }); -}); - -QUnit.test("Admin viewing group messages when there are no messages", assert => { - server.get('/topics/private-messages-group/eviltrout/discourse.json', () => { // eslint-disable-line no-undef - return response({ topic_list: { topics: [] } }); - }); - - logIn(); - Discourse.reset(); - - visit("/groups/discourse"); - - click(".nav-pills li a[title='Messages']"); + click(".group-message-button"); andThen(() => { + assert.ok(count("#reply-control") === 1, "it opens the composer"); assert.equal( - find(".alert").text().trim(), - I18n.t('choose_topic.none_found'), - 'it should display the right alert' + find(".ac-wrap .item").text(), + "discourse", + "it prefills the group name" ); }); }); +QUnit.test( + "Admin viewing group messages when there are no messages", + assert => { + // prettier-ignore + server.get("/topics/private-messages-group/eviltrout/discourse.json", () => { // eslint-disable-line no-undef + return response({ topic_list: { topics: [] } }); + }); + + logIn(); + Discourse.reset(); + + visit("/groups/discourse"); + + click(".nav-pills li a[title='Messages']"); + + andThen(() => { + assert.equal( + find(".alert") + .text() + .trim(), + I18n.t("choose_topic.none_found"), + "it should display the right alert" + ); + }); + } +); + QUnit.test("Admin viewing group messages", assert => { - server.get('/topics/private-messages-group/eviltrout/discourse.json', () => { // eslint-disable-line no-undef - return response({"users":[{"id":2, "username":"bruce1", "avatar_template":"/user_avatar/meta.discourse.org/bruce1/{size}/5245.png"}, {"id":3, "username":"CodingHorror", "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5245.png"}], "primary_groups":[], "topic_list":{"can_create_topic":true, "draft":null, "draft_key":"new_topic", "draft_sequence":0, "per_page":30, "topics":[{"id":12199, "title":"This is a private message 1", "fancy_title":"This is a private message 1", "slug":"this-is-a-private-message-1", "posts_count":0, "reply_count":0, "highest_post_number":0, "image_url":null, "created_at":"2018-03-16T03:38:45.583Z", "last_posted_at":null, "bumped":true, "bumped_at":"2018-03-16T03:38:45.583Z", "unseen":false, "pinned":false, "unpinned":null, "visible":true, "closed":false, "archived":false, "bookmarked":null, "liked":null, "views":0, "like_count":0, "has_summary":false, "archetype":"private_message", "last_poster_username":"bruce1", "category_id":null, "pinned_globally":false, "featured_link":null, "posters":[{"extras":"latest single", "description":"Original Poster, Most Recent Poster", "user_id":2, "primary_group_id":null}], "participants":[{"extras":"latest", "description":null, "user_id":2, "primary_group_id":null}, {"extras":null, "description":null, "user_id":3, "primary_group_id":null}]}]}}); + // prettier-ignore + server.get("/topics/private-messages-group/eviltrout/discourse.json", () => { // eslint-disable-line no-undef + return response({ + users: [ + { + id: 2, + username: "bruce1", + avatar_template: + "/user_avatar/meta.discourse.org/bruce1/{size}/5245.png" + }, + { + id: 3, + username: "CodingHorror", + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5245.png" + } + ], + primary_groups: [], + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + draft_sequence: 0, + per_page: 30, + topics: [ + { + id: 12199, + title: "This is a private message 1", + fancy_title: "This is a private message 1", + slug: "this-is-a-private-message-1", + posts_count: 0, + reply_count: 0, + highest_post_number: 0, + image_url: null, + created_at: "2018-03-16T03:38:45.583Z", + last_posted_at: null, + bumped: true, + bumped_at: "2018-03-16T03:38:45.583Z", + unseen: false, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 0, + like_count: 0, + has_summary: false, + archetype: "private_message", + last_poster_username: "bruce1", + category_id: null, + pinned_globally: false, + featured_link: null, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 2, + primary_group_id: null + } + ], + participants: [ + { + extras: "latest", + description: null, + user_id: 2, + primary_group_id: null + }, + { + extras: null, + description: null, + user_id: 3, + primary_group_id: null + } + ] + } + ] + } + }); }); logIn(); @@ -151,9 +257,11 @@ QUnit.test("Admin viewing group messages", assert => { andThen(() => { assert.equal( - find(".topic-list-item .link-top-line").text().trim(), + find(".topic-list-item .link-top-line") + .text() + .trim(), "This is a private message 1", - 'it should display the list of group topics' + "it should display the list of group topics" ); }); }); @@ -167,10 +275,18 @@ QUnit.test("Admin Viewing Group", assert => { andThen(() => { assert.ok( find(".nav-pills li a[title='Manage']").length === 1, - 'it should show manage group tab if user is admin' + "it should show manage group tab if user is admin" ); - assert.equal(count('.group-message-button'), 1, 'it displays show group message button'); - assert.equal(find('.group-info-name').text(), 'Awesome Team', 'it should display the group name'); + assert.equal( + count(".group-message-button"), + 1, + "it displays show group message button" + ); + assert.equal( + find(".group-info-name").text(), + "Awesome Team", + "it should display the group name" + ); }); }); diff --git a/test/javascripts/acceptance/groups-index-test.js.es6 b/test/javascripts/acceptance/groups-index-test.js.es6 index 503372269f..5429434ba6 100644 --- a/test/javascripts/acceptance/groups-index-test.js.es6 +++ b/test/javascripts/acceptance/groups-index-test.js.es6 @@ -6,38 +6,52 @@ QUnit.test("Browsing Groups", assert => { visit("/groups"); andThen(() => { - assert.equal(count('.groups-table-row'), 2, 'it displays visible groups'); - assert.equal(find('.group-index-join').length, 1, 'it shows button to join group'); - assert.equal(find('.group-index-request').length, 1, 'it shows button to request for group membership'); + assert.equal(count(".groups-table-row"), 2, "it displays visible groups"); + assert.equal( + find(".group-index-join").length, + 1, + "it shows button to join group" + ); + assert.equal( + find(".group-index-request").length, + 1, + "it shows button to request for group membership" + ); }); - click('.group-index-join'); + click(".group-index-join"); andThen(() => { - assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); + assert.ok(exists(".modal.login-modal"), "it shows the login modal"); }); - click('.login-modal .close'); + click(".login-modal .close"); andThen(() => { - assert.ok(invisible('.modal.login-modal'), 'it closes the login modal'); + assert.ok(invisible(".modal.login-modal"), "it closes the login modal"); }); - click('.group-index-request'); + click(".group-index-request"); andThen(() => { - assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); + assert.ok(exists(".modal.login-modal"), "it shows the login modal"); }); click("a[href='/groups/discourse/members']"); andThen(() => { - assert.equal(find('.group-info-name').text().trim(), 'Awesome Team', "it displays the group page"); + assert.equal( + find(".group-info-name") + .text() + .trim(), + "Awesome Team", + "it displays the group page" + ); }); - click('.group-index-join'); + click(".group-index-join"); andThen(() => { - assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); + assert.ok(exists(".modal.login-modal"), "it shows the login modal"); }); }); diff --git a/test/javascripts/acceptance/groups-new-test.js.es6 b/test/javascripts/acceptance/groups-new-test.js.es6 index 423f189abd..96e02e4129 100644 --- a/test/javascripts/acceptance/groups-new-test.js.es6 +++ b/test/javascripts/acceptance/groups-new-test.js.es6 @@ -7,8 +7,9 @@ QUnit.test("As an anon user", assert => { andThen(() => { assert.equal( - find('.groups-header-new').length, 0, - 'it should not display the button to create a group' + find(".groups-header-new").length, + 0, + "it should not display the button to create a group" ); }); }); @@ -23,49 +24,65 @@ QUnit.test("Creating a new group", assert => { andThen(() => { assert.equal( - find('.group-form-save[disabled]').length, 1, - 'save button should be disabled' + find(".group-form-save[disabled]").length, + 1, + "save button should be disabled" ); }); - fillIn("input[name='name']", '1'); + fillIn("input[name='name']", "1"); andThen(() => { assert.equal( - find('.tip.bad').text().trim(), I18n.t("admin.groups.new.name.too_short"), - 'it should show the right validation tooltip' + find(".tip.bad") + .text() + .trim(), + I18n.t("admin.groups.new.name.too_short"), + "it should show the right validation tooltip" ); assert.ok( find(".group-form-save:disabled").length === 1, - 'it should disable the save button' + "it should disable the save button" ); }); - fillIn("input[name='name']", 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + fillIn( + "input[name='name']", + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ); andThen(() => { assert.equal( - find('.tip.bad').text().trim(), I18n.t("admin.groups.new.name.too_long"), - 'it should show the right validation tooltip' + find(".tip.bad") + .text() + .trim(), + I18n.t("admin.groups.new.name.too_long"), + "it should show the right validation tooltip" ); }); - fillIn("input[name='name']", ''); + fillIn("input[name='name']", ""); andThen(() => { assert.equal( - find('.tip.bad').text().trim(), I18n.t("admin.groups.new.name.blank"), - 'it should show the right validation tooltip' + find(".tip.bad") + .text() + .trim(), + I18n.t("admin.groups.new.name.blank"), + "it should show the right validation tooltip" ); }); - fillIn("input[name='name']", 'goodusername'); + fillIn("input[name='name']", "goodusername"); andThen(() => { assert.equal( - find('.tip.good').text().trim(), I18n.t("admin.groups.new.name.available"), - 'it should show the right validation tooltip' + find(".tip.good") + .text() + .trim(), + I18n.t("admin.groups.new.name.available"), + "it should show the right validation tooltip" ); }); @@ -73,8 +90,9 @@ QUnit.test("Creating a new group", assert => { andThen(() => { assert.equal( - find('groups-new-allow-membership-requests').length, 0, - 'it should disable the membership requests checkbox' + find("groups-new-allow-membership-requests").length, + 0, + "it should disable the membership requests checkbox" ); }); }); diff --git a/test/javascripts/acceptance/invite-accept-test.js.es6 b/test/javascripts/acceptance/invite-accept-test.js.es6 index 697948e69d..024b5ac9d7 100644 --- a/test/javascripts/acceptance/invite-accept-test.js.es6 +++ b/test/javascripts/acceptance/invite-accept-test.js.es6 @@ -1,5 +1,5 @@ import { acceptance } from "helpers/qunit-helpers"; -import PreloadStore from 'preload-store'; +import PreloadStore from "preload-store"; acceptance("Invite Accept", { settings: { @@ -8,8 +8,14 @@ acceptance("Invite Accept", { }); QUnit.test("Invite Acceptance Page", assert => { - PreloadStore.store('invite_info', { - invited_by: {"id":123,"username":"neil","avatar_template":"/user_avatar/localhost/neil/{size}/25_1.png","name":"Neil Lalonde","title":"team"}, + PreloadStore.store("invite_info", { + invited_by: { + id: 123, + username: "neil", + avatar_template: "/user_avatar/localhost/neil/{size}/25_1.png", + name: "Neil Lalonde", + title: "team" + }, email: "invited@asdf.com", username: "invited" }); @@ -17,34 +23,53 @@ QUnit.test("Invite Acceptance Page", assert => { visit("/invites/myvalidinvitetoken"); andThen(() => { assert.ok(exists("#new-account-username"), "shows the username input"); - assert.equal(find("#new-account-username").val(), "invited", "username is prefilled"); + assert.equal( + find("#new-account-username").val(), + "invited", + "username is prefilled" + ); assert.ok(exists("#new-account-name"), "shows the name input"); assert.ok(exists("#new-account-password"), "shows the password input"); - assert.ok(exists('.invites-show .btn-primary:disabled'), 'submit is disabled because name is not filled'); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "submit is disabled because name is not filled" + ); }); - fillIn("#new-account-name", 'John Doe'); + fillIn("#new-account-name", "John Doe"); andThen(() => { - assert.not(exists('.invites-show .btn-primary:disabled'), 'submit is enabled'); + assert.not( + exists(".invites-show .btn-primary:disabled"), + "submit is enabled" + ); }); - fillIn("#new-account-username", 'a'); + fillIn("#new-account-username", "a"); andThen(() => { assert.ok(exists(".username-input .bad"), "username is not valid"); - assert.ok(exists('.invites-show .btn-primary:disabled'), 'submit is disabled'); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "submit is disabled" + ); }); - fillIn("#new-account-password", 'aaa'); + fillIn("#new-account-password", "aaa"); andThen(() => { assert.ok(exists(".password-input .bad"), "password is not valid"); - assert.ok(exists('.invites-show .btn-primary:disabled'), 'submit is disabled'); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "submit is disabled" + ); }); - fillIn("#new-account-username", 'validname'); - fillIn("#new-account-password", 'secur3ty4Y0uAndMe'); + fillIn("#new-account-username", "validname"); + fillIn("#new-account-password", "secur3ty4Y0uAndMe"); andThen(() => { assert.ok(exists(".username-input .good"), "username is valid"); assert.ok(exists(".password-input .good"), "password is valid"); - assert.not(exists('.invites-show .btn-primary:disabled'), 'submit is enabled'); + assert.not( + exists(".invites-show .btn-primary:disabled"), + "submit is enabled" + ); }); }); diff --git a/test/javascripts/acceptance/invite-show-user-fields-test.js.es6 b/test/javascripts/acceptance/invite-show-user-fields-test.js.es6 index 823a5113ce..f007687955 100644 --- a/test/javascripts/acceptance/invite-show-user-fields-test.js.es6 +++ b/test/javascripts/acceptance/invite-show-user-fields-test.js.es6 @@ -2,9 +2,26 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("Accept Invite - User Fields", { site: { - user_fields: [{"id":34,"name":"I've read the terms of service","field_type":"confirm","required":true}, - {"id":35,"name":"What is your pet's name?","field_type":"text","required":true}, - {"id":36,"name":"What's your dad like?","field_type":"text","required":false}] + user_fields: [ + { + id: 34, + name: "I've read the terms of service", + field_type: "confirm", + required: true + }, + { + id: 35, + name: "What is your pet's name?", + field_type: "text", + required: true + }, + { + id: 36, + name: "What's your dad like?", + field_type: "text", + required: false + } + ] } }); @@ -12,33 +29,47 @@ QUnit.test("accept invite with user fields", assert => { visit("/invites/myvalidinvitetoken"); andThen(() => { assert.ok(exists(".invites-show"), "shows the accept invite page"); - assert.ok(exists('.user-field'), "it has at least one user field"); - assert.ok(exists('.invites-show .btn-primary:disabled'), 'submit is disabled'); + assert.ok(exists(".user-field"), "it has at least one user field"); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "submit is disabled" + ); }); - fillIn("#new-account-name", 'John Doe'); - fillIn("#new-account-username", 'validname'); - fillIn("#new-account-password", 'secur3ty4Y0uAndMe'); + fillIn("#new-account-name", "John Doe"); + fillIn("#new-account-username", "validname"); + fillIn("#new-account-password", "secur3ty4Y0uAndMe"); andThen(() => { assert.ok(exists(".username-input .good"), "username is valid"); - assert.ok(exists('.invites-show .btn-primary:disabled'), 'submit is still disabled due to lack of user fields'); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "submit is still disabled due to lack of user fields" + ); }); fillIn(".user-field input[type=text]:first", "Barky"); andThen(() => { - assert.ok(exists('.invites-show .btn-primary:disabled'), 'submit is disabled because field is not checked'); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "submit is disabled because field is not checked" + ); }); click(".user-field input[type=checkbox]"); andThen(() => { - assert.not(exists('.invites-show .btn-primary:disabled'), 'submit is enabled because field is checked'); + assert.not( + exists(".invites-show .btn-primary:disabled"), + "submit is enabled because field is checked" + ); }); click(".user-field input[type=checkbox]"); andThen(() => { - assert.ok(exists('.invites-show .btn-primary:disabled'), 'unclicking the checkbox disables the submit'); + assert.ok( + exists(".invites-show .btn-primary:disabled"), + "unclicking the checkbox disables the submit" + ); }); - }); diff --git a/test/javascripts/acceptance/login-required-test.js.es6 b/test/javascripts/acceptance/login-required-test.js.es6 index 6153b12f22..f508e9612f 100644 --- a/test/javascripts/acceptance/login-required-test.js.es6 +++ b/test/javascripts/acceptance/login-required-test.js.es6 @@ -7,23 +7,27 @@ acceptance("Login Required", { }); QUnit.test("redirect", assert => { - visit('/latest'); + visit("/latest"); andThen(() => { assert.equal(currentPath(), "login", "it redirects them to login"); }); - click('#site-logo'); + click("#site-logo"); andThen(() => { - assert.equal(currentPath(), "login", "clicking the logo keeps them on login"); + assert.equal( + currentPath(), + "login", + "clicking the logo keeps them on login" + ); }); - click('header .login-button'); + click("header .login-button"); andThen(() => { - assert.ok(exists('.login-modal'), "they can still access the login modal"); + assert.ok(exists(".login-modal"), "they can still access the login modal"); }); - click('.modal-header .close'); + click(".modal-header .close"); andThen(() => { - assert.ok(invisible('.login-modal'), "it closes the login modal"); + assert.ok(invisible(".login-modal"), "it closes the login modal"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/login-with-email-test.js.es6 b/test/javascripts/acceptance/login-with-email-test.js.es6 index ecd8f161d7..d7e6c4342c 100644 --- a/test/javascripts/acceptance/login-with-email-test.js.es6 +++ b/test/javascripts/acceptance/login-with-email-test.js.es6 @@ -9,15 +9,12 @@ acceptance("Login with email", { }, beforeEach() { const response = object => { - return [ - 200, - { "Content-Type": "application/json" }, - object - ]; + return [200, { "Content-Type": "application/json" }, object]; }; - server.post('/u/email-login', () => { // eslint-disable-line no-undef - return response({ "success": "OK", "user_found": userFound }); + // prettier-ignore + server.post("/u/email-login", () => { // eslint-disable-line no-undef + return response({ success: "OK", user_found: userFound }); }); } }); @@ -27,7 +24,10 @@ QUnit.test("logging in via email (link)", assert => { click("header .login-button"); andThen(() => { - assert.notOk(exists(".login-with-email-link"), 'it displays the link only when field is filled'); + assert.notOk( + exists(".login-with-email-link"), + "it displays the link only when field is filled" + ); userFound = false; }); @@ -37,48 +37,58 @@ QUnit.test("logging in via email (link)", assert => { andThen(() => { assert.equal( find(".alert-error").html(), - I18n.t('email_login.complete_username_not_found', { username: 'someuser' }), - 'it should display an error for an invalid username' + I18n.t("email_login.complete_username_not_found", { + username: "someuser" + }), + "it should display an error for an invalid username" ); }); - fillIn("#login-account-name", 'someuser@gmail.com'); - click('.login-with-email-link'); + fillIn("#login-account-name", "someuser@gmail.com"); + click(".login-with-email-link"); andThen(() => { assert.equal( find(".alert-error").html(), - I18n.t('email_login.complete_email_not_found', { email: 'someuser@gmail.com' }), - 'it should display an error for an invalid email' + I18n.t("email_login.complete_email_not_found", { + email: "someuser@gmail.com" + }), + "it should display an error for an invalid email" ); }); - fillIn("#login-account-name", 'someuser'); + fillIn("#login-account-name", "someuser"); andThen(() => { userFound = true; }); - click('.login-with-email-link'); + click(".login-with-email-link"); andThen(() => { assert.equal( - find(".alert-success").html().trim(), - I18n.t('email_login.complete_username_found', { username: 'someuser' }), - 'it should display a success message for a valid username' + find(".alert-success") + .html() + .trim(), + I18n.t("email_login.complete_username_found", { username: "someuser" }), + "it should display a success message for a valid username" ); }); visit("/"); click("header .login-button"); - fillIn("#login-account-name", 'someuser@gmail.com'); - click('.login-with-email-link'); + fillIn("#login-account-name", "someuser@gmail.com"); + click(".login-with-email-link"); andThen(() => { assert.equal( - find(".alert-success").html().trim(), - I18n.t('email_login.complete_email_found', { email: 'someuser@gmail.com' }), - 'it should display a success message for a valid email' + find(".alert-success") + .html() + .trim(), + I18n.t("email_login.complete_email_found", { + email: "someuser@gmail.com" + }), + "it should display a success message for a valid email" ); }); @@ -90,13 +100,13 @@ QUnit.test("logging in via email (link)", assert => { QUnit.test("logging in via email (button)", assert => { visit("/"); click("header .login-button"); - click('.login-with-email-button'); + click(".login-with-email-button"); andThen(() => { assert.equal( find(".alert-error").html(), - I18n.t('login.blank_username'), - 'it should display an error for blank username' + I18n.t("login.blank_username"), + "it should display an error for blank username" ); }); @@ -104,14 +114,16 @@ QUnit.test("logging in via email (button)", assert => { userFound = true; }); - fillIn("#login-account-name", 'someuser'); - click('.login-with-email-button'); + fillIn("#login-account-name", "someuser"); + click(".login-with-email-button"); andThen(() => { assert.equal( - find(".alert-success").html().trim(), - I18n.t('email_login.complete_username_found', { username: 'someuser' }), - 'it should display a success message for a valid username' + find(".alert-success") + .html() + .trim(), + I18n.t("email_login.complete_username_found", { username: "someuser" }), + "it should display a success message for a valid username" ); }); }); @@ -124,15 +136,12 @@ acceptance("Login with email", { }, beforeEach() { const response = object => { - return [ - 200, - { "Content-Type": "application/json" }, - object - ]; + return [200, { "Content-Type": "application/json" }, object]; }; - server.post('/u/email-login', () => { // eslint-disable-line no-undef - return response({ "success": "OK" }); + // prettier-ignore + server.post("/u/email-login", () => { // eslint-disable-line no-undef + return response({ success: "OK" }); }); } }); @@ -140,14 +149,18 @@ acceptance("Login with email", { QUnit.test("login via email with hide_email_address_taken enabled", assert => { visit("/"); click("header .login-button"); - fillIn("#login-account-name", 'someuser@example.com'); - click('.login-with-email-button'); + fillIn("#login-account-name", "someuser@example.com"); + click(".login-with-email-button"); andThen(() => { assert.equal( - find(".alert-success").html().trim(), - I18n.t('email_login.complete_email_found', { email: 'someuser@example.com' }), - 'it should display the success message for any email address' + find(".alert-success") + .html() + .trim(), + I18n.t("email_login.complete_email_found", { + email: "someuser@example.com" + }), + "it should display the success message for any email address" ); }); }); diff --git a/test/javascripts/acceptance/mobile-discovery-test.js.es6 b/test/javascripts/acceptance/mobile-discovery-test.js.es6 index 564c5b4ec1..76237d227b 100644 --- a/test/javascripts/acceptance/mobile-discovery-test.js.es6 +++ b/test/javascripts/acceptance/mobile-discovery-test.js.es6 @@ -5,11 +5,11 @@ QUnit.test("Visit Discovery Pages", assert => { visit("/"); andThen(() => { assert.ok(exists(".topic-list"), "The list of topics was rendered"); - assert.ok(exists('.topic-list .topic-list-item'), "has topics"); + assert.ok(exists(".topic-list .topic-list-item"), "has topics"); }); visit("/categories"); andThen(() => { - assert.ok(exists('.category'), "has a list of categories"); + assert.ok(exists(".category"), "has a list of categories"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/mobile-sign-in-test.js.es6 b/test/javascripts/acceptance/mobile-sign-in-test.js.es6 index 34c03ec7d8..7d3760d8fb 100644 --- a/test/javascripts/acceptance/mobile-sign-in-test.js.es6 +++ b/test/javascripts/acceptance/mobile-sign-in-test.js.es6 @@ -6,6 +6,6 @@ QUnit.test("sign in", assert => { visit("/"); click("header .login-button"); andThen(() => { - assert.ok(exists('#login-form'), "it shows the login modal"); + assert.ok(exists("#login-form"), "it shows the login modal"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/mobile-users-test.js.es6 b/test/javascripts/acceptance/mobile-users-test.js.es6 index 70d5f2a0c5..f94226e4ac 100644 --- a/test/javascripts/acceptance/mobile-users-test.js.es6 +++ b/test/javascripts/acceptance/mobile-users-test.js.es6 @@ -5,6 +5,6 @@ acceptance("User Directory - Mobile", { mobileView: true }); QUnit.test("Visit Page", assert => { visit("/users"); andThen(() => { - assert.ok(exists('.directory .user'), "has a list of users"); + assert.ok(exists(".directory .user"), "has a list of users"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/modal-test.js.es6 b/test/javascripts/acceptance/modal-test.js.es6 index 7907c12b21..9075a29f73 100644 --- a/test/javascripts/acceptance/modal-test.js.es6 +++ b/test/javascripts/acceptance/modal-test.js.es6 @@ -1,30 +1,64 @@ import { acceptance } from "helpers/qunit-helpers"; +import showModal from "discourse/lib/show-modal"; + acceptance("Modal"); QUnit.test("modal", assert => { - visit('/'); + visit("/"); andThen(() => { - assert.ok(find('.d-modal:visible').length === 0, 'there is no modal at first'); + assert.ok( + find(".d-modal:visible").length === 0, + "there is no modal at first" + ); }); - click('.login-button'); + click(".login-button"); andThen(() => { - assert.ok(find('.d-modal:visible').length === 1, 'modal should appear'); + assert.ok(find(".d-modal:visible").length === 1, "modal should appear"); }); - click('.modal-outer-container'); + click(".modal-outer-container"); andThen(() => { - assert.ok(find('.d-modal:visible').length === 0, 'modal should disappear when you click outside'); + assert.ok( + find(".d-modal:visible").length === 0, + "modal should disappear when you click outside" + ); }); - click('.login-button'); + click(".login-button"); andThen(() => { - assert.ok(find('.d-modal:visible').length === 1, 'modal should reappear'); + assert.ok(find(".d-modal:visible").length === 1, "modal should reappear"); }); - keyEvent('#main-outlet', 'keydown', 27); + keyEvent("#main-outlet", "keydown", 27); andThen(() => { - assert.ok(find('.d-modal:visible').length === 0, 'ESC should close the modal'); + assert.ok( + find(".d-modal:visible").length === 0, + "ESC should close the modal" + ); + }); + andThen(() => { + Ember.TEMPLATES["modal/not-dismissable"] = Ember.HTMLBars.compile( + '{{#d-modal-body title="" class="" dismissable=false}}test{{/d-modal-body}}' + ); + showModal("not-dismissable", {}); + }); + andThen(() => { + assert.ok(find(".d-modal:visible").length === 1, "modal should appear"); + }); + click(".modal-outer-container"); + andThen(() => { + assert.ok( + find(".d-modal:visible").length === 1, + "modal should not disappear when you click outside" + ); + }); + keyEvent("#main-outlet", "keydown", 27); + andThen(() => { + assert.ok( + find(".d-modal:visible").length === 1, + "ESC should not close the modal" + ); }); }); diff --git a/test/javascripts/acceptance/password-reset-test.js.es6 b/test/javascripts/acceptance/password-reset-test.js.es6 index b105315b47..fa8f8ab6e7 100644 --- a/test/javascripts/acceptance/password-reset-test.js.es6 +++ b/test/javascripts/acceptance/password-reset-test.js.es6 @@ -1,38 +1,50 @@ import { acceptance } from "helpers/qunit-helpers"; -import PreloadStore from 'preload-store'; +import PreloadStore from "preload-store"; import { parsePostData } from "helpers/create-pretender"; acceptance("Password Reset", { beforeEach() { - const response = (object) => { - return [ - 200, - {"Content-Type": "application/json"}, - object - ]; + const response = object => { + return [200, { "Content-Type": "application/json" }, object]; }; - server.get('/u/confirm-email-token/myvalidtoken.json', () => { //eslint-disable-line - return response({success: "OK"}); - }); - - server.put('/u/password-reset/myvalidtoken.json', request => { //eslint-disable-line - const body = parsePostData(request.requestBody); - if (body.password === "jonesyAlienSlayer") { - return response({success: false, errors: {password: ["is the name of your cat"]}}); - } else { - return response({success: "OK", message: I18n.t('password_reset.success')}); - } - }); - - server.get('/u/confirm-email-token/requiretwofactor.json', () => { //eslint-disable-line + // prettier-ignore + server.get("/u/confirm-email-token/myvalidtoken.json", () => { //eslint-disable-line return response({ success: "OK" }); }); - server.put('/u/password-reset/requiretwofactor.json', request => { //eslint-disable-line + // prettier-ignore + server.put("/u/password-reset/myvalidtoken.json", request => { //eslint-disable-line const body = parsePostData(request.requestBody); - if (body.password === "perf3ctly5ecur3" && body.second_factor_token === "123123") { - return response({ success: "OK", message: I18n.t('password_reset.success') }); + if (body.password === "jonesyAlienSlayer") { + return response({ + success: false, + errors: { password: ["is the name of your cat"] } + }); + } else { + return response({ + success: "OK", + message: I18n.t("password_reset.success") + }); + } + }); + + // prettier-ignore + server.get("/u/confirm-email-token/requiretwofactor.json", () => { //eslint-disable-line + return response({ success: "OK" }); + }); + + // prettier-ignore + server.put("/u/password-reset/requiretwofactor.json", request => { //eslint-disable-line + const body = parsePostData(request.requestBody); + if ( + body.password === "perf3ctly5ecur3" && + body.second_factor_token === "123123" + ) { + return response({ + success: "OK", + message: I18n.t("password_reset.success") + }); } else if (body.second_factor_token === "123123") { return response({ success: false, errors: { password: ["invalid"] } }); } else { @@ -47,40 +59,50 @@ acceptance("Password Reset", { }); QUnit.test("Password Reset Page", assert => { - PreloadStore.store('password_reset', {is_developer: false}); + PreloadStore.store("password_reset", { is_developer: false }); visit("/u/password-reset/myvalidtoken"); andThen(() => { assert.ok(exists(".password-reset input"), "shows the input"); }); - fillIn('.password-reset input', 'perf3ctly5ecur3'); + fillIn(".password-reset input", "perf3ctly5ecur3"); andThen(() => { assert.ok(exists(".password-reset .tip.good"), "input looks good"); }); - fillIn('.password-reset input', '123'); + fillIn(".password-reset input", "123"); andThen(() => { assert.ok(exists(".password-reset .tip.bad"), "input is not valid"); - assert.ok(find(".password-reset .tip.bad").html().indexOf(I18n.t('user.password.too_short')) > -1, "password too short"); + assert.ok( + find(".password-reset .tip.bad") + .html() + .indexOf(I18n.t("user.password.too_short")) > -1, + "password too short" + ); }); - fillIn('.password-reset input', 'jonesyAlienSlayer'); - click('.password-reset form button'); + fillIn(".password-reset input", "jonesyAlienSlayer"); + click(".password-reset form button"); andThen(() => { assert.ok(exists(".password-reset .tip.bad"), "input is not valid"); - assert.ok(find(".password-reset .tip.bad").html().indexOf("is the name of your cat") > -1, "server validation error message shows"); + assert.ok( + find(".password-reset .tip.bad") + .html() + .indexOf("is the name of your cat") > -1, + "server validation error message shows" + ); }); - fillIn('.password-reset input', 'perf3ctly5ecur3'); - click('.password-reset form button'); + fillIn(".password-reset input", "perf3ctly5ecur3"); + click(".password-reset form button"); andThen(() => { assert.ok(!exists(".password-reset form"), "form is gone"); }); }); QUnit.test("Password Reset Page With Second Factor", assert => { - PreloadStore.store('password_reset', { + PreloadStore.store("password_reset", { is_developer: false, second_factor_required: true }); @@ -92,28 +114,30 @@ QUnit.test("Password Reset Page With Second Factor", assert => { assert.ok(exists("#second-factor"), "shows the second factor prompt"); }); - fillIn('#second-factor', '0000'); - click('.password-reset form button'); + fillIn("#second-factor", "0000"); + click(".password-reset form button"); andThen(() => { assert.ok(exists(".alert-error"), "shows 2 factor error"); assert.ok( - find(".alert-error").html().indexOf("invalid token") > -1, + find(".alert-error") + .html() + .indexOf("invalid token") > -1, "shows server validation error message" ); }); - fillIn('#second-factor', '123123'); - click('.password-reset form button'); + fillIn("#second-factor", "123123"); + click(".password-reset form button"); andThen(() => { assert.notOk(exists(".alert-error"), "hides error"); assert.ok(exists("#new-account-password"), "shows the input"); }); - fillIn('.password-reset input', 'perf3ctly5ecur3'); - click('.password-reset form button'); + fillIn(".password-reset input", "perf3ctly5ecur3"); + click(".password-reset form button"); andThen(() => { assert.ok(!exists(".password-reset form"), "form is gone"); diff --git a/test/javascripts/acceptance/personal-message-test.js.es6 b/test/javascripts/acceptance/personal-message-test.js.es6 index eb31bf048e..c10c3a5d17 100644 --- a/test/javascripts/acceptance/personal-message-test.js.es6 +++ b/test/javascripts/acceptance/personal-message-test.js.es6 @@ -8,7 +8,10 @@ QUnit.test("footer edit button", assert => { visit("/t/pm-for-testing/12"); andThen(() => { - assert.ok(!exists('.edit-message'), 'does not show edit first post button on footer by default'); + assert.ok( + !exists(".edit-message"), + "does not show edit first post button on footer by default" + ); }); }); @@ -21,6 +24,9 @@ QUnit.test("show footer edit button", assert => { visit("/t/pm-for-testing/12"); andThen(() => { - assert.ok(exists('.edit-message'), 'shows edit first post button on footer when PM tagging is enabled'); + assert.ok( + exists(".edit-message"), + "shows edit first post button on footer when PM tagging is enabled" + ); }); }); diff --git a/test/javascripts/acceptance/plugin-outlet-connector-class-test.js.es6 b/test/javascripts/acceptance/plugin-outlet-connector-class-test.js.es6 index b7a371d1e3..8f4a18f730 100644 --- a/test/javascripts/acceptance/plugin-outlet-connector-class-test.js.es6 +++ b/test/javascripts/acceptance/plugin-outlet-connector-class-test.js.es6 @@ -1,31 +1,33 @@ import { acceptance } from "helpers/qunit-helpers"; -import { extraConnectorClass } from 'discourse/lib/plugin-connectors'; +import { extraConnectorClass } from "discourse/lib/plugin-connectors"; const PREFIX = "javascripts/single-test/connectors"; acceptance("Plugin Outlet - Connector Class", { beforeEach() { - extraConnectorClass('user-profile-primary/hello', { + extraConnectorClass("user-profile-primary/hello", { actions: { sayHello() { - this.set('hello', 'hello!'); + this.set("hello", "hello!"); } } }); - extraConnectorClass('user-profile-primary/dont-render', { + extraConnectorClass("user-profile-primary/dont-render", { shouldRender(args) { - return args.model.get('username') !== 'eviltrout'; + return args.model.get("username") !== "eviltrout"; } }); - Ember.TEMPLATES[`${PREFIX}/user-profile-primary/hello`] = Ember.HTMLBars.compile( + Ember.TEMPLATES[ + `${PREFIX}/user-profile-primary/hello` + ] = Ember.HTMLBars.compile( `{{model.username}} {{hello}}` ); - Ember.TEMPLATES[`${PREFIX}/user-profile-primary/dont-render`] = Ember.HTMLBars.compile( - `I'm not rendered!` - ); + Ember.TEMPLATES[ + `${PREFIX}/user-profile-primary/dont-render` + ] = Ember.HTMLBars.compile(`I'm not rendered!`); }, afterEach() { @@ -37,11 +39,21 @@ acceptance("Plugin Outlet - Connector Class", { QUnit.test("Renders a template into the outlet", assert => { visit("/u/eviltrout"); andThen(() => { - assert.ok(find('.user-profile-primary-outlet.hello').length === 1, 'it has class names'); - assert.ok(!find('.user-profile-primary-outlet.dont-render').length, "doesn't render"); + assert.ok( + find(".user-profile-primary-outlet.hello").length === 1, + "it has class names" + ); + assert.ok( + !find(".user-profile-primary-outlet.dont-render").length, + "doesn't render" + ); }); - click('.say-hello'); + click(".say-hello"); andThen(() => { - assert.equal(find('.hello-result').text(), 'hello!', 'actions delegate properly'); + assert.equal( + find(".hello-result").text(), + "hello!", + "actions delegate properly" + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/plugin-outlet-multi-template-test.js.es6 b/test/javascripts/acceptance/plugin-outlet-multi-template-test.js.es6 index 3f4722d757..280cb88dd5 100644 --- a/test/javascripts/acceptance/plugin-outlet-multi-template-test.js.es6 +++ b/test/javascripts/acceptance/plugin-outlet-multi-template-test.js.es6 @@ -1,14 +1,19 @@ import { acceptance } from "helpers/qunit-helpers"; -import { clearCache } from 'discourse/lib/plugin-connectors'; +import { clearCache } from "discourse/lib/plugin-connectors"; -const HELLO = 'javascripts/multi-test/connectors/user-profile-primary/hello'; -const GOODBYE = 'javascripts/multi-test/connectors/user-profile-primary/goodbye'; +const HELLO = "javascripts/multi-test/connectors/user-profile-primary/hello"; +const GOODBYE = + "javascripts/multi-test/connectors/user-profile-primary/goodbye"; acceptance("Plugin Outlet - Multi Template", { beforeEach() { clearCache(); - Ember.TEMPLATES[HELLO] = Ember.HTMLBars.compile(`Hello`); - Ember.TEMPLATES[GOODBYE] = Ember.HTMLBars.compile(`Goodbye`); + Ember.TEMPLATES[HELLO] = Ember.HTMLBars.compile( + `Hello` + ); + Ember.TEMPLATES[GOODBYE] = Ember.HTMLBars.compile( + `Goodbye` + ); }, afterEach() { @@ -21,9 +26,23 @@ acceptance("Plugin Outlet - Multi Template", { QUnit.test("Renders a template into the outlet", assert => { visit("/u/eviltrout"); andThen(() => { - assert.ok(find('.user-profile-primary-outlet.hello').length === 1, 'it has class names'); - assert.ok(find('.user-profile-primary-outlet.goodbye').length === 1, 'it has class names'); - assert.equal(find('.hello-span').text(), 'Hello', 'it renders into the outlet'); - assert.equal(find('.bye-span').text(), 'Goodbye', 'it renders into the outlet'); + assert.ok( + find(".user-profile-primary-outlet.hello").length === 1, + "it has class names" + ); + assert.ok( + find(".user-profile-primary-outlet.goodbye").length === 1, + "it has class names" + ); + assert.equal( + find(".hello-span").text(), + "Hello", + "it renders into the outlet" + ); + assert.equal( + find(".bye-span").text(), + "Goodbye", + "it renders into the outlet" + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/plugin-outlet-single-template-test.js.es6 b/test/javascripts/acceptance/plugin-outlet-single-template-test.js.es6 index 48ae7ba07c..6d74c0a9ff 100644 --- a/test/javascripts/acceptance/plugin-outlet-single-template-test.js.es6 +++ b/test/javascripts/acceptance/plugin-outlet-single-template-test.js.es6 @@ -1,6 +1,7 @@ import { acceptance } from "helpers/qunit-helpers"; -const CONNECTOR = 'javascripts/single-test/connectors/user-profile-primary/hello'; +const CONNECTOR = + "javascripts/single-test/connectors/user-profile-primary/hello"; acceptance("Plugin Outlet - Single Template", { beforeEach() { Ember.TEMPLATES[CONNECTOR] = Ember.HTMLBars.compile( @@ -16,7 +17,14 @@ acceptance("Plugin Outlet - Single Template", { QUnit.test("Renders a template into the outlet", assert => { visit("/u/eviltrout"); andThen(() => { - assert.ok(find('.user-profile-primary-outlet.hello').length === 1, 'it has class names'); - assert.equal(find('.hello-username').text(), 'eviltrout', 'it renders into the outlet'); + assert.ok( + find(".user-profile-primary-outlet.hello").length === 1, + "it has class names" + ); + assert.equal( + find(".hello-username").text(), + "eviltrout", + "it renders into the outlet" + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/preferences-test.js.es6 b/test/javascripts/acceptance/preferences-test.js.es6 index ac9dd14529..962ce8bade 100644 --- a/test/javascripts/acceptance/preferences-test.js.es6 +++ b/test/javascripts/acceptance/preferences-test.js.es6 @@ -2,23 +2,21 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("User Preferences", { loggedIn: true, beforeEach() { - const response = (object) => { - return [ - 200, - {"Content-Type": "application/json"}, - object - ]; + const response = object => { + return [200, { "Content-Type": "application/json" }, object]; }; - server.post('/u/second_factors.json', () => { //eslint-disable-line + // prettier-ignore + server.post("/u/second_factors.json", () => { //eslint-disable-line return response({ key: "rcyryaqage3jexfj", qr: '
qr-code
' }); }); - server.put('/u/second_factor.json', () => { //eslint-disable-line - return response({ error: 'invalid token' }); + // prettier-ignore + server.put("/u/second_factor.json", () => { //eslint-disable-line + return response({ error: "invalid token" }); }); } }); @@ -27,16 +25,20 @@ QUnit.test("update some fields", assert => { visit("/u/eviltrout/preferences"); andThen(() => { - assert.ok($('body.user-preferences-page').length, "has the body class"); - assert.equal(currentURL(), '/u/eviltrout/preferences/account', "defaults to account tab"); - assert.ok(exists('.user-preferences'), 'it shows the preferences'); + assert.ok($("body.user-preferences-page").length, "has the body class"); + assert.equal( + currentURL(), + "/u/eviltrout/preferences/account", + "defaults to account tab" + ); + assert.ok(exists(".user-preferences"), "it shows the preferences"); }); const savePreferences = () => { - click('.save-user'); - assert.ok(!exists('.saved-user'), "it hasn't been saved yet"); + click(".save-user"); + assert.ok(!exists(".saved-user"), "it hasn't been saved yet"); andThen(() => { - assert.ok(exists('.saved-user'), 'it displays the saved message'); + assert.ok(exists(".saved-user"), "it displays the saved message"); }); }; @@ -52,21 +54,29 @@ QUnit.test("update some fields", assert => { savePreferences(); click(".preferences-nav .nav-notifications a"); - selectKit('.control-group.notifications .combo-box.duration').expand().selectRowByValue(1440); + selectKit(".control-group.notifications .combo-box.duration") + .expand() + .selectRowByValue(1440); savePreferences(); click(".preferences-nav .nav-categories a"); - fillIn('.category-controls .category-selector', 'faq'); + fillIn(".category-controls .category-selector", "faq"); savePreferences(); - assert.ok(!exists('.preferences-nav .nav-tags a'), "tags tab isn't there when tags are disabled"); + assert.ok( + !exists(".preferences-nav .nav-tags a"), + "tags tab isn't there when tags are disabled" + ); // Error: Unhandled request in test environment: /themes/assets/10d71596-7e4e-4dc0-b368-faa3b6f1ce6d?_=1493833562388 (GET) // click(".preferences-nav .nav-interface a"); // click('.control-group.other input[type=checkbox]:first'); // savePreferences(); - assert.ok(!exists('.preferences-nav .nav-apps a'), "apps tab isn't there when you have no authorized apps"); + assert.ok( + !exists(".preferences-nav .nav-apps a"), + "apps tab isn't there when you have no authorized apps" + ); }); QUnit.test("username", assert => { @@ -89,10 +99,16 @@ QUnit.test("email", assert => { assert.ok(exists("#change-email"), "it has the input element"); }); - fillIn("#change-email", 'invalidemail'); + fillIn("#change-email", "invalidemail"); andThen(() => { - assert.equal(find('.tip.bad').text().trim(), I18n.t('user.email.invalid'), 'it should display invalid email tip'); + assert.equal( + find(".tip.bad") + .text() + .trim(), + I18n.t("user.email.invalid"), + "it should display invalid email tip" + ); }); }); @@ -103,7 +119,7 @@ QUnit.test("second factor", assert => { assert.ok(exists("#password"), "it has a password input"); }); - fillIn('#password', 'secrets'); + fillIn("#password", "secrets"); click(".user-content .btn-primary"); andThen(() => { @@ -111,12 +127,14 @@ QUnit.test("second factor", assert => { assert.notOk(exists("#password"), "it hides the password input"); }); - fillIn("#second-factor-token", '111111'); - click('.btn-primary'); + fillIn("#second-factor-token", "111111"); + click(".btn-primary"); andThen(() => { assert.ok( - find(".alert-error").html().indexOf("invalid token") > -1, + find(".alert-error") + .html() + .indexOf("invalid token") > -1, "shows server validation error message" ); }); @@ -132,8 +150,12 @@ acceptance("User Preferences when badges are disabled", { QUnit.test("visit my preferences", assert => { visit("/u/eviltrout/preferences"); andThen(() => { - assert.ok($('body.user-preferences-page').length, "has the body class"); - assert.equal(currentURL(), '/u/eviltrout/preferences/account', "defaults to account tab"); - assert.ok(exists('.user-preferences'), 'it shows the preferences'); + assert.ok($("body.user-preferences-page").length, "has the body class"); + assert.equal( + currentURL(), + "/u/eviltrout/preferences/account", + "defaults to account tab" + ); + assert.ok(exists(".user-preferences"), "it shows the preferences"); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/queued-posts-test.js.es6 b/test/javascripts/acceptance/queued-posts-test.js.es6 index c2349ce01a..2e62d365ed 100644 --- a/test/javascripts/acceptance/queued-posts-test.js.es6 +++ b/test/javascripts/acceptance/queued-posts-test.js.es6 @@ -5,33 +5,174 @@ acceptance("Queued Posts", { settings: { tagging_enabled: true } }); -QUnit.test("For topics: body of post, title, category and tags are all editbale", assert => { - server.get("/queued_posts", () => { //eslint-disable-line no-undef - return [ - 200, - {"Content-Type": "application/json"}, - {"users":[{"id":3,"username":"test_user","avatar_template":"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png","active":true,"admin":false,"moderator":false,"last_seen_at":"2017-08-11T20:48:05.405Z","last_emailed_at":null,"created_at":"2017-08-07T02:23:33.309Z","last_seen_age":"1d","last_emailed_age":null,"created_at_age":"6d","username_lower":"test_user","trust_level":0,"trust_level_locked":false,"flag_level":0,"title":null,"suspended_at":null,"suspended_till":null,"suspended":null,"silenced":false,"time_read":"19m","staged":false,"days_visited":4,"posts_read_count":12,"topics_entered":6,"post_count":2}],"queued_posts":[{"id":22,"queue":"default","user_id":3,"state":1,"topic_id":null,"approved_by_id":null,"rejected_by_id":null,"raw":"some content","post_options":{"archetype":"regular","category":"1","typing_duration_msecs":"3200","composer_open_duration_msecs":"19007","visible":true,"is_warning":false,"title":"a new topic that needs to be reviewed","ip_address":"172.17.0.1","first_post_checks":true,"is_poll":true},"created_at":"2017-08-11T20:43:41.115Z","category_id":1,"can_delete_user":true}],"__rest_serializer":"1","refresh_queued_posts":"/queued_posts?status=new"} - ]; - }); +QUnit.test( + "For topics: body of post, title, category and tags are all editbale", + assert => { + // prettier-ignore + server.get("/queued_posts", () => { //eslint-disable-line no-undef + return [ + 200, + { "Content-Type": "application/json" }, + { + users: [ + { + id: 3, + username: "test_user", + avatar_template: + "/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png", + active: true, + admin: false, + moderator: false, + last_seen_at: "2017-08-11T20:48:05.405Z", + last_emailed_at: null, + created_at: "2017-08-07T02:23:33.309Z", + last_seen_age: "1d", + last_emailed_age: null, + created_at_age: "6d", + username_lower: "test_user", + trust_level: 0, + trust_level_locked: false, + flag_level: 0, + title: null, + suspended_at: null, + suspended_till: null, + suspended: null, + silenced: false, + time_read: "19m", + staged: false, + days_visited: 4, + posts_read_count: 12, + topics_entered: 6, + post_count: 2 + } + ], + queued_posts: [ + { + id: 22, + queue: "default", + user_id: 3, + state: 1, + topic_id: null, + approved_by_id: null, + rejected_by_id: null, + raw: "some content", + post_options: { + archetype: "regular", + category: "1", + typing_duration_msecs: "3200", + composer_open_duration_msecs: "19007", + visible: true, + is_warning: false, + title: "a new topic that needs to be reviewed", + ip_address: "172.17.0.1", + first_post_checks: true, + is_poll: true + }, + created_at: "2017-08-11T20:43:41.115Z", + category_id: 1, + can_delete_user: true + } + ], + __rest_serializer: "1", + refresh_queued_posts: "/queued_posts?status=new" + } + ]; + }); - visit("/queued-posts"); - click(".queued-posts .queued-post button.edit"); - - andThen(() => { - assert.ok(exists(".d-editor-container"), "the body should be editable"); - assert.ok(exists(".edit-title .ember-text-field"), "the title should be editable"); - assert.ok(exists(".category-chooser"), "category should be editbale"); - assert.ok(exists(".tag-chooser"), "tags should be editable"); - }); -}); + visit("/queued-posts"); + click(".queued-posts .queued-post button.edit"); + andThen(() => { + assert.ok(exists(".d-editor-container"), "the body should be editable"); + assert.ok( + exists(".edit-title .ember-text-field"), + "the title should be editable" + ); + assert.ok(exists(".category-chooser"), "category should be editbale"); + assert.ok(exists(".tag-chooser"), "tags should be editable"); + }); + } +); QUnit.test("For replies: only the body of post is editbale", assert => { + // prettier-ignore server.get("/queued_posts", () => { //eslint-disable-line no-undef return [ 200, - {"Content-Type": "application/json"}, - {"users":[{"id":3,"username":"test_user","avatar_template":"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png","active":true,"admin":false,"moderator":false,"last_seen_at":"2017-08-11T20:48:05.405Z","last_emailed_at":null,"created_at":"2017-08-07T02:23:33.309Z","last_seen_age":"1d","last_emailed_age":null,"created_at_age":"6d","username_lower":"test_user","trust_level":0,"trust_level_locked":false,"flag_level":0,"title":null,"suspended_at":null,"suspended_till":null,"suspended":null,"silenced":false,"time_read":"19m","staged":false,"days_visited":4,"posts_read_count":12,"topics_entered":6,"post_count":2}],"topics":[{"id":11,"title":"This is a topic","fancy_title":"This is a topic","slug":"this-is-a-topic","posts_count":2}],"queued_posts":[{"id":4,"queue":"default","user_id":3,"state":1,"topic_id":11,"approved_by_id":null,"rejected_by_id":null,"raw":"edited haahaasdfasdfasdfasdf","post_options":{"archetype":"regular","category":"3","reply_to_post_number":"2","typing_duration_msecs":"1900","composer_open_duration_msecs":"12096","visible":true,"is_warning":false,"featured_link":"","ip_address":"172.17.0.1","first_post_checks":true,"is_poll":true},"created_at":"2017-08-07T19:11:52.018Z","category_id":3,"can_delete_user":true}],"__rest_serializer":"1","refresh_queued_posts":"/queued_posts?status=new"} + { "Content-Type": "application/json" }, + { + users: [ + { + id: 3, + username: "test_user", + avatar_template: + "/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png", + active: true, + admin: false, + moderator: false, + last_seen_at: "2017-08-11T20:48:05.405Z", + last_emailed_at: null, + created_at: "2017-08-07T02:23:33.309Z", + last_seen_age: "1d", + last_emailed_age: null, + created_at_age: "6d", + username_lower: "test_user", + trust_level: 0, + trust_level_locked: false, + flag_level: 0, + title: null, + suspended_at: null, + suspended_till: null, + suspended: null, + silenced: false, + time_read: "19m", + staged: false, + days_visited: 4, + posts_read_count: 12, + topics_entered: 6, + post_count: 2 + } + ], + topics: [ + { + id: 11, + title: "This is a topic", + fancy_title: "This is a topic", + slug: "this-is-a-topic", + posts_count: 2 + } + ], + queued_posts: [ + { + id: 4, + queue: "default", + user_id: 3, + state: 1, + topic_id: 11, + approved_by_id: null, + rejected_by_id: null, + raw: "edited haahaasdfasdfasdfasdf", + post_options: { + archetype: "regular", + category: "3", + reply_to_post_number: "2", + typing_duration_msecs: "1900", + composer_open_duration_msecs: "12096", + visible: true, + is_warning: false, + featured_link: "", + ip_address: "172.17.0.1", + first_post_checks: true, + is_poll: true + }, + created_at: "2017-08-07T19:11:52.018Z", + category_id: 3, + can_delete_user: true + } + ], + __rest_serializer: "1", + refresh_queued_posts: "/queued_posts?status=new" + } ]; }); @@ -40,8 +181,14 @@ QUnit.test("For replies: only the body of post is editbale", assert => { andThen(() => { assert.ok(exists(".d-editor-container"), "the body should be editable"); - assert.notOk(exists(".edit-title .ember-text-field"), "title should not be editbale"); - assert.notOk(exists(".category-chooser"), "category should not be editable"); + assert.notOk( + exists(".edit-title .ember-text-field"), + "title should not be editbale" + ); + assert.notOk( + exists(".category-chooser"), + "category should not be editable" + ); assert.notOk(exists("div.tag-chooser"), "tags should not be editable"); }); }); diff --git a/test/javascripts/acceptance/raw-plugin-outlet-test.js.es6 b/test/javascripts/acceptance/raw-plugin-outlet-test.js.es6 index 5303d53cfe..ceabaf776b 100644 --- a/test/javascripts/acceptance/raw-plugin-outlet-test.js.es6 +++ b/test/javascripts/acceptance/raw-plugin-outlet-test.js.es6 @@ -1,6 +1,7 @@ import { acceptance } from "helpers/qunit-helpers"; -const CONNECTOR = 'javascripts/raw-test/connectors/topic-list-before-status/lala'; +const CONNECTOR = + "javascripts/raw-test/connectors/topic-list-before-status/lala"; acceptance("Raw Plugin Outlet", { beforeEach() { Discourse.RAW_TEMPLATES[CONNECTOR] = Handlebars.compile( @@ -16,7 +17,11 @@ acceptance("Raw Plugin Outlet", { QUnit.test("Renders the raw plugin outlet", assert => { visit("/"); andThen(() => { - assert.ok(find('.topic-lala').length > 0, 'it renders the outlet'); - assert.equal(find('.topic-lala:eq(0)').text(), '11557', 'it has the topic id'); + assert.ok(find(".topic-lala").length > 0, "it renders the outlet"); + assert.equal( + find(".topic-lala:eq(0)").text(), + "11557", + "it has the topic id" + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/reports-test.js.es6 b/test/javascripts/acceptance/reports-test.js.es6 new file mode 100644 index 0000000000..55f4030ae1 --- /dev/null +++ b/test/javascripts/acceptance/reports-test.js.es6 @@ -0,0 +1,31 @@ +import { acceptance } from "helpers/qunit-helpers"; + +acceptance("Reports", { + loggedIn: true +}); + +QUnit.test("Visit reports page", assert => { + visit("/admin/reports"); + + andThen(() => { + assert.equal($(".reports-list .report").length, 1); + + const $report = $(".reports-list .report:first-child"); + + assert.equal( + $report + .find(".report-title") + .html() + .trim(), + "My report" + ); + + assert.equal( + $report + .find(".report-description") + .html() + .trim(), + "List of my activities" + ); + }); +}); diff --git a/test/javascripts/acceptance/search-full-test.js.es6 b/test/javascripts/acceptance/search-full-test.js.es6 index 61566c00bd..aa76791710 100644 --- a/test/javascripts/acceptance/search-full-test.js.es6 +++ b/test/javascripts/acceptance/search-full-test.js.es6 @@ -1,112 +1,195 @@ import { acceptance, waitFor } from "helpers/qunit-helpers"; acceptance("Search - Full Page", { - settings: {tagging_enabled: true}, + settings: { tagging_enabled: true }, loggedIn: true, beforeEach() { - const response = (object) => { - return [ - 200, - {"Content-Type": "application/json"}, - object - ]; + const response = object => { + return [200, { "Content-Type": "application/json" }, object]; }; - server.get('/tags/filter/search', () => { //eslint-disable-line - return response({results: [{text: 'monkey', count: 1}]}); + // prettier-ignore + server.get("/tags/filter/search", () => { //eslint-disable-line + return response({ results: [{ text: "monkey", count: 1 }] }); }); - server.get('/u/search/users', () => { //eslint-disable-line - return response({users: [{username: "admin", name: "admin", - avatar_template: "/images/avatar.png"}]}); + // prettier-ignore + server.get("/u/search/users", () => { //eslint-disable-line + return response({ + users: [ + { + username: "admin", + name: "admin", + avatar_template: "/images/avatar.png" + } + ] + }); }); - server.get('/admin/groups.json', () => { //eslint-disable-line - return response([{id: 2, automatic: true, name: "moderators", user_count: 4, alias_level: 0, - visible: true, automatic_membership_email_domains: null, automatic_membership_retroactive: false, - primary_group: false, title: null, grant_trust_level: null, incoming_email: null, - notification_level: null, has_messages: true, is_member: true, mentionable: false, - flair_url: null, flair_bg_color: null, flair_color: null}]); + // prettier-ignore + server.get("/admin/groups.json", () => { //eslint-disable-line + return response([ + { + id: 2, + automatic: true, + name: "moderators", + user_count: 4, + alias_level: 0, + visible: true, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + notification_level: null, + has_messages: true, + is_member: true, + mentionable: false, + flair_url: null, + flair_bg_color: null, + flair_color: null + } + ]); }); - server.get('/badges.json', () => { //eslint-disable-line - return response({badge_types: [{id: 3, name: "Bronze", sort_order: 7}], - badge_groupings: [{id: 1, name: "Getting Started", description: null, position: 10, system: true}], - badges: [{id: 17, name: "Reader", description: "Read every reply in a topic with more than 100 replies", - grant_count: 0, allow_title: false, multiple_grant: false, icon: "fa-certificate", image: null, - listable: true, enabled: true, badge_grouping_id: 1, system: true, - long_description: "This badge is granted the first time you read a long topic with more than 100 replies. Reading a conversation closely helps you follow the discussion, understand different viewpoints, and leads to more interesting conversations. The more you read, the better the conversation gets. As we like to say, Reading is Fundamental! :slight_smile:\n", - slug: "reader", has_badge: false, badge_type_id: 3}]}); + // prettier-ignore + server.get("/badges.json", () => { //eslint-disable-line + return response({ + badge_types: [{ id: 3, name: "Bronze", sort_order: 7 }], + badge_groupings: [ + { + id: 1, + name: "Getting Started", + description: null, + position: 10, + system: true + } + ], + badges: [ + { + id: 17, + name: "Reader", + description: + "Read every reply in a topic with more than 100 replies", + grant_count: 0, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + image: null, + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + long_description: + "This badge is granted the first time you read a long topic with more than 100 replies. Reading a conversation closely helps you follow the discussion, understand different viewpoints, and leads to more interesting conversations. The more you read, the better the conversation gets. As we like to say, Reading is Fundamental! :slight_smile:\n", + slug: "reader", + has_badge: false, + badge_type_id: 3 + } + ] + }); }); } }); - QUnit.test("perform various searches", assert => { visit("/search"); andThen(() => { - assert.ok($('body.search-page').length, "has body class"); - assert.ok(exists('.search-container'), "has container class"); - assert.ok(find('.search-query').length > 0); - assert.ok(find('.fps-topic').length === 0); + assert.ok($("body.search-page").length, "has body class"); + assert.ok(exists(".search-container"), "has container class"); + assert.ok(find(".search-query").length > 0); + assert.ok(find(".fps-topic").length === 0); }); - fillIn('.search-query', 'none'); - click('.search-cta'); + fillIn(".search-query", "none"); + click(".search-cta"); andThen(() => { - assert.ok(find('.fps-topic').length === 0, 'has no results'); - assert.ok(find('.no-results-suggestion .google-search-form')); + assert.ok(find(".fps-topic").length === 0, "has no results"); + assert.ok(find(".no-results-suggestion .google-search-form")); }); - fillIn('.search-query', 'posts'); - click('.search-cta'); + fillIn(".search-query", "posts"); + click(".search-cta"); - andThen(() => assert.ok(find('.fps-topic').length === 1, 'has one post')); + andThen(() => assert.ok(find(".fps-topic").length === 1, "has one post")); }); -QUnit.test("escape search term", (assert) => { +QUnit.test("escape search term", assert => { visit("/search"); - fillIn('.search-query', '@gmail.com'); + fillIn(".search-query", "@gmail.com"); andThen(() => { - assert.ok(exists('.search-advanced-options span:contains("gmail.com")'), 'it escapes search term'); + assert.ok( + exists( + '.search-advanced-options span:contains("gmail.com")' + ), + "it escapes search term" + ); }); }); QUnit.test("update username through advanced search ui", assert => { visit("/search"); - fillIn('.search-query', 'none'); - fillIn('.search-advanced-options .user-selector', 'admin'); - click('.search-advanced-options .user-selector'); - keyEvent('.search-advanced-options .user-selector', 'keydown', 8); + fillIn(".search-query", "none"); + fillIn(".search-advanced-options .user-selector", "admin"); + click(".search-advanced-options .user-selector"); + keyEvent(".search-advanced-options .user-selector", "keydown", 8); andThen(() => { waitFor(assert, () => { - assert.ok(visible('.search-advanced-options .autocomplete'), '"autocomplete" popup is visible'); - assert.ok(exists('.search-advanced-options .autocomplete ul li a span.username:contains("admin")'), '"autocomplete" popup has an entry for "admin"'); + assert.ok( + visible(".search-advanced-options .autocomplete"), + '"autocomplete" popup is visible' + ); + assert.ok( + exists( + '.search-advanced-options .autocomplete ul li a span.username:contains("admin")' + ), + '"autocomplete" popup has an entry for "admin"' + ); - click('.search-advanced-options .autocomplete ul li a:first'); + click(".search-advanced-options .autocomplete ul li a:first"); andThen(() => { - assert.ok(exists('.search-advanced-options span:contains("admin")'), 'has "admin" pre-populated'); - assert.equal(find('.search-query').val(), "none @admin", 'has updated search term to "none user:admin"'); + assert.ok( + exists('.search-advanced-options span:contains("admin")'), + 'has "admin" pre-populated' + ); + assert.equal( + find(".search-query").val(), + "none @admin", + 'has updated search term to "none user:admin"' + ); }); }); }); }); QUnit.test("update category through advanced search ui", assert => { - const categoryChooser = selectKit('.search-advanced-options .category-chooser'); + const categoryChooser = selectKit( + ".search-advanced-options .category-chooser" + ); visit("/search"); - fillIn('.search-query', 'none'); - categoryChooser.expand().fillInFilter('faq').selectRowByValue(4); + fillIn(".search-query", "none"); + categoryChooser + .expand() + .fillInFilter("faq") + .selectRowByValue(4); andThen(() => { - assert.ok(exists('.search-advanced-options .badge-category:contains("faq")'), 'has "faq" populated'); - assert.equal(find('.search-query').val(), "none #faq", 'has updated search term to "none #faq"'); + assert.ok( + exists('.search-advanced-options .badge-category:contains("faq")'), + 'has "faq" populated' + ); + assert.equal( + find(".search-query").val(), + "none #faq", + 'has updated search term to "none #faq"' + ); }); }); @@ -176,109 +259,181 @@ QUnit.test("update category through advanced search ui", assert => { // }); // }); // -QUnit.test("update in:title filter through advanced search ui", async assert => { - await visit("/search"); - await fillIn('.search-query', 'none'); - await click('.search-advanced-options .in-title'); +QUnit.test( + "update in:title filter through advanced search ui", + async assert => { + await visit("/search"); + await fillIn(".search-query", "none"); + await click(".search-advanced-options .in-title"); - assert.ok(exists('.search-advanced-options .in-title:checked'), 'has "in title" populated'); - assert.equal(find('.search-query').val(), "none in:title", 'has updated search term to "none in:title"'); -}); + assert.ok( + exists(".search-advanced-options .in-title:checked"), + 'has "in title" populated' + ); + assert.equal( + find(".search-query").val(), + "none in:title", + 'has updated search term to "none in:title"' + ); + } +); -QUnit.test("update in:likes filter through advanced search ui", async assert => { - await visit("/search"); - await fillIn('.search-query', 'none'); - await click('.search-advanced-options .in-likes'); +QUnit.test( + "update in:likes filter through advanced search ui", + async assert => { + await visit("/search"); + await fillIn(".search-query", "none"); + await click(".search-advanced-options .in-likes"); - assert.ok(exists('.search-advanced-options .in-likes:checked'), 'has "I liked" populated'); - assert.equal(find('.search-query').val(), "none in:likes", 'has updated search term to "none in:likes"'); -}); + assert.ok( + exists(".search-advanced-options .in-likes:checked"), + 'has "I liked" populated' + ); + assert.equal( + find(".search-query").val(), + "none in:likes", + 'has updated search term to "none in:likes"' + ); + } +); QUnit.test("update in:private filter through advanced search ui", assert => { visit("/search"); - fillIn('.search-query', 'none'); - click('.search-advanced-options .in-private'); + fillIn(".search-query", "none"); + click(".search-advanced-options .in-private"); andThen(() => { - assert.ok(exists('.search-advanced-options .in-private:checked'), 'has "are in my messages" populated'); - assert.equal(find('.search-query').val(), "none in:private", 'has updated search term to "none in:private"'); + assert.ok( + exists(".search-advanced-options .in-private:checked"), + 'has "are in my messages" populated' + ); + assert.equal( + find(".search-query").val(), + "none in:private", + 'has updated search term to "none in:private"' + ); }); }); QUnit.test("update in:seen filter through advanced search ui", assert => { visit("/search"); - fillIn('.search-query', 'none'); - click('.search-advanced-options .in-seen'); + fillIn(".search-query", "none"); + click(".search-advanced-options .in-seen"); andThen(() => { - assert.ok(exists('.search-advanced-options .in-seen:checked'), 'it should check the right checkbox'); + assert.ok( + exists(".search-advanced-options .in-seen:checked"), + "it should check the right checkbox" + ); - assert.equal(find('.search-query').val(), "none in:seen", - 'it should update the search term' + assert.equal( + find(".search-query").val(), + "none in:seen", + "it should update the search term" ); }); }); QUnit.test("update in filter through advanced search ui", assert => { - const inSelector = selectKit('.search-advanced-options .select-kit#in'); + const inSelector = selectKit(".search-advanced-options .select-kit#in"); visit("/search"); - fillIn('.search-query', 'none'); - inSelector.expand().selectRowByValue('bookmarks'); + fillIn(".search-query", "none"); + inSelector.expand().selectRowByValue("bookmarks"); andThen(() => { - assert.ok(inSelector.rowByName("I bookmarked").exists(), 'has "I bookmarked" populated'); - assert.equal(find('.search-query').val(), "none in:bookmarks", 'has updated search term to "none in:bookmarks"'); + assert.ok( + inSelector.rowByName("I bookmarked").exists(), + 'has "I bookmarked" populated' + ); + assert.equal( + find(".search-query").val(), + "none in:bookmarks", + 'has updated search term to "none in:bookmarks"' + ); }); }); QUnit.test("update status through advanced search ui", assert => { - const statusSelector = selectKit('.search-advanced-options .select-kit#status'); + const statusSelector = selectKit( + ".search-advanced-options .select-kit#status" + ); visit("/search"); - fillIn('.search-query', 'none'); - statusSelector.expand().selectRowByValue('closed'); + fillIn(".search-query", "none"); + statusSelector.expand().selectRowByValue("closed"); andThen(() => { - assert.ok(statusSelector.rowByName("are closed").exists(), 'has "are closed" populated'); - assert.equal(find('.search-query').val(), "none status:closed", 'has updated search term to "none status:closed"'); + assert.ok( + statusSelector.rowByName("are closed").exists(), + 'has "are closed" populated' + ); + assert.equal( + find(".search-query").val(), + "none status:closed", + 'has updated search term to "none status:closed"' + ); }); }); QUnit.test("update post time through advanced search ui", assert => { - const postTimeSelector = selectKit('.search-advanced-options .select-kit#postTime'); + const postTimeSelector = selectKit( + ".search-advanced-options .select-kit#postTime" + ); visit("/search"); - fillIn('.search-query', 'none'); - fillIn('#search-post-date .date-picker', '2016-10-05'); - postTimeSelector.expand().selectRowByValue('after'); + fillIn(".search-query", "none"); + fillIn("#search-post-date .date-picker", "2016-10-05"); + postTimeSelector.expand().selectRowByValue("after"); andThen(() => { - assert.ok(postTimeSelector.rowByName("after").exists(), 'has "after" populated'); - assert.equal(find('.search-query').val(), "none after:2016-10-05", 'has updated search term to "none after:2016-10-05"'); + assert.ok( + postTimeSelector.rowByName("after").exists(), + 'has "after" populated' + ); + assert.equal( + find(".search-query").val(), + "none after:2016-10-05", + 'has updated search term to "none after:2016-10-05"' + ); }); }); QUnit.test("update min post count through advanced search ui", assert => { visit("/search"); - fillIn('.search-query', 'none'); - fillIn('#search-min-post-count', '5'); + fillIn(".search-query", "none"); + fillIn("#search-min-post-count", "5"); andThen(() => { - assert.equal(find('.search-advanced-options #search-min-post-count').val(), "5", 'has "5" populated'); - assert.equal(find('.search-query').val(), "none min_post_count:5", 'has updated search term to "none min_post_count:5"'); + assert.equal( + find(".search-advanced-options #search-min-post-count").val(), + "5", + 'has "5" populated' + ); + assert.equal( + find(".search-query").val(), + "none min_post_count:5", + 'has updated search term to "none min_post_count:5"' + ); }); }); QUnit.test("validate advanced search when initially empty", assert => { visit("/search?expanded=true"); - click('.search-advanced-options .in-likes'); + click(".search-advanced-options .in-likes"); andThen(() => { - assert.ok(exists('.search-advanced-options .in-likes:checked'), 'has "I liked" populated'); - assert.equal(find('.search-query').val(), "in:likes", 'has updated search term to "in:likes"'); + assert.ok( + exists(".search-advanced-options .in-likes:checked"), + 'has "I liked" populated' + ); + assert.equal( + find(".search-query").val(), + "in:likes", + 'has updated search term to "in:likes"' + ); }); }); diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 index d388d67787..b8d463977d 100644 --- a/test/javascripts/acceptance/search-test.js.es6 +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -1,124 +1,140 @@ import { acceptance, logIn } from "helpers/qunit-helpers"; acceptance("Search"); -QUnit.test("search", (assert) => { +QUnit.test("search", assert => { visit("/"); - click('#search-button'); + click("#search-button"); andThen(() => { - assert.ok(exists('#search-term'), 'it shows the search bar'); - assert.ok(!exists('.search-menu .results ul li'), 'no results by default'); + assert.ok(exists("#search-term"), "it shows the search bar"); + assert.ok(!exists(".search-menu .results ul li"), "no results by default"); }); - fillIn('#search-term', 'dev'); - keyEvent('#search-term', 'keyup', 16); + fillIn("#search-term", "dev"); + keyEvent("#search-term", "keyup", 16); andThen(() => { - assert.ok(exists('.search-menu .results ul li'), 'it shows results'); + assert.ok(exists(".search-menu .results ul li"), "it shows results"); }); - click('.show-help'); + click(".show-help"); andThen(() => { - assert.equal(find('.full-page-search').val(), 'dev', 'it shows the search term'); - assert.ok(exists('.search-advanced-options'), 'advanced search is expanded'); + assert.equal( + find(".full-page-search").val(), + "dev", + "it shows the search term" + ); + assert.ok( + exists(".search-advanced-options"), + "advanced search is expanded" + ); }); }); -QUnit.test("search for a tag", (assert) => { +QUnit.test("search for a tag", assert => { visit("/"); - click('#search-button'); + click("#search-button"); - fillIn('#search-term', 'evil'); - keyEvent('#search-term', 'keyup', 16); + fillIn("#search-term", "evil"); + keyEvent("#search-term", "keyup", 16); andThen(() => { - assert.ok(exists('.search-menu .results ul li'), 'it shows results'); + assert.ok(exists(".search-menu .results ul li"), "it shows results"); }); }); QUnit.test("search scope checkbox", assert => { visit("/c/bug"); - click('#search-button'); + click("#search-button"); andThen(() => { - assert.ok(exists('.search-context input:checked'), 'scope to category checkbox is checked'); + assert.ok( + exists(".search-context input:checked"), + "scope to category checkbox is checked" + ); }); - click('#search-button'); + click("#search-button"); visit("/t/internationalization-localization/280"); - click('#search-button'); + click("#search-button"); andThen(() => { - assert.not(exists('.search-context input:checked'), 'scope to topic checkbox is not checked'); + assert.not( + exists(".search-context input:checked"), + "scope to topic checkbox is not checked" + ); }); - click('#search-button'); + click("#search-button"); visit("/u/eviltrout"); - click('#search-button'); + click("#search-button"); andThen(() => { - assert.ok(exists('.search-context input:checked'), 'scope to user checkbox is checked'); + assert.ok( + exists(".search-context input:checked"), + "scope to user checkbox is checked" + ); }); }); QUnit.test("Search with context", assert => { visit("/t/internationalization-localization/280/1"); - click('#search-button'); - fillIn('#search-term', 'dev'); + click("#search-button"); + fillIn("#search-term", "dev"); click(".search-context input[type='checkbox']"); - keyEvent('#search-term', 'keyup', 16); + keyEvent("#search-term", "keyup", 16); andThen(() => { - assert.ok(exists('.search-menu .results ul li'), 'it shows results'); + assert.ok(exists(".search-menu .results ul li"), "it shows results"); assert.ok( - exists('.cooked span.highlight-strong'), - 'it should highlight the search term' + exists(".cooked span.highlight-strong"), + "it should highlight the search term" ); }); visit("/"); - click('#search-button'); + click("#search-button"); andThen(() => { assert.ok(!exists(".search-context input[type='checkbox']")); }); visit("/t/internationalization-localization/280/1"); - click('#search-button'); + click("#search-button"); andThen(() => { - assert.ok(!$('.search-context input[type=checkbox]').is(":checked")); + assert.ok(!$(".search-context input[type=checkbox]").is(":checked")); }); }); QUnit.test("Right filters are shown to anonymous users", assert => { - const inSelector = selectKit('.select-kit#in'); + const inSelector = selectKit(".select-kit#in"); visit("/search?expanded=true"); inSelector.expand(); andThen(() => { - assert.ok(inSelector.rowByValue('first').exists()); - assert.ok(inSelector.rowByValue('pinned').exists()); - assert.ok(inSelector.rowByValue('unpinned').exists()); - assert.ok(inSelector.rowByValue('wiki').exists()); - assert.ok(inSelector.rowByValue('images').exists()); + assert.ok(inSelector.rowByValue("first").exists()); + assert.ok(inSelector.rowByValue("pinned").exists()); + assert.ok(inSelector.rowByValue("unpinned").exists()); + assert.ok(inSelector.rowByValue("wiki").exists()); + assert.ok(inSelector.rowByValue("images").exists()); - assert.notOk(inSelector.rowByValue('unseen').exists()); - assert.notOk(inSelector.rowByValue('posted').exists()); - assert.notOk(inSelector.rowByValue('watching').exists()); - assert.notOk(inSelector.rowByValue('tracking').exists()); - assert.notOk(inSelector.rowByValue('bookmarks').exists()); + assert.notOk(inSelector.rowByValue("unseen").exists()); + assert.notOk(inSelector.rowByValue("posted").exists()); + assert.notOk(inSelector.rowByValue("watching").exists()); + assert.notOk(inSelector.rowByValue("tracking").exists()); + assert.notOk(inSelector.rowByValue("bookmarks").exists()); - assert.notOk(exists('.search-advanced-options .in-likes')); - assert.notOk(exists('.search-advanced-options .in-private')); - assert.notOk(exists('.search-advanced-options .in-seen')); + assert.notOk(exists(".search-advanced-options .in-likes")); + assert.notOk(exists(".search-advanced-options .in-private")); + assert.notOk(exists(".search-advanced-options .in-seen")); }); }); QUnit.test("Right filters are shown to logged-in users", assert => { - const inSelector = selectKit('.select-kit#in'); + const inSelector = selectKit(".select-kit#in"); logIn(); Discourse.reset(); @@ -127,20 +143,20 @@ QUnit.test("Right filters are shown to logged-in users", assert => { inSelector.expand(); andThen(() => { - assert.ok(inSelector.rowByValue('first').exists()); - assert.ok(inSelector.rowByValue('pinned').exists()); - assert.ok(inSelector.rowByValue('unpinned').exists()); - assert.ok(inSelector.rowByValue('wiki').exists()); - assert.ok(inSelector.rowByValue('images').exists()); + assert.ok(inSelector.rowByValue("first").exists()); + assert.ok(inSelector.rowByValue("pinned").exists()); + assert.ok(inSelector.rowByValue("unpinned").exists()); + assert.ok(inSelector.rowByValue("wiki").exists()); + assert.ok(inSelector.rowByValue("images").exists()); - assert.ok(inSelector.rowByValue('unseen').exists()); - assert.ok(inSelector.rowByValue('posted').exists()); - assert.ok(inSelector.rowByValue('watching').exists()); - assert.ok(inSelector.rowByValue('tracking').exists()); - assert.ok(inSelector.rowByValue('bookmarks').exists()); + assert.ok(inSelector.rowByValue("unseen").exists()); + assert.ok(inSelector.rowByValue("posted").exists()); + assert.ok(inSelector.rowByValue("watching").exists()); + assert.ok(inSelector.rowByValue("tracking").exists()); + assert.ok(inSelector.rowByValue("bookmarks").exists()); - assert.ok(exists('.search-advanced-options .in-likes')); - assert.ok(exists('.search-advanced-options .in-private')); - assert.ok(exists('.search-advanced-options .in-seen')); + assert.ok(exists(".search-advanced-options .in-likes")); + assert.ok(exists(".search-advanced-options .in-private")); + assert.ok(exists(".search-advanced-options .in-seen")); }); }); diff --git a/test/javascripts/acceptance/shared-drafts-test.js.es6 b/test/javascripts/acceptance/shared-drafts-test.js.es6 index 6aea6ec38b..28f2ce0ca3 100644 --- a/test/javascripts/acceptance/shared-drafts-test.js.es6 +++ b/test/javascripts/acceptance/shared-drafts-test.js.es6 @@ -2,18 +2,18 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("Shared Drafts", { loggedIn: true }); -QUnit.test('Viewing', assert => { +QUnit.test("Viewing", assert => { visit("/t/some-topic/9"); andThen(() => { - assert.ok(find('.shared-draft-controls').length === 1); - let categoryChooser = selectKit('.shared-draft-controls .category-chooser'); - assert.equal(categoryChooser.header().value(), '3'); + assert.ok(find(".shared-draft-controls").length === 1); + let categoryChooser = selectKit(".shared-draft-controls .category-chooser"); + assert.equal(categoryChooser.header().value(), "3"); }); - click('.publish-shared-draft'); - click('.bootbox .btn-primary'); + click(".publish-shared-draft"); + click(".bootbox .btn-primary"); andThen(() => { - assert.ok(find('.shared-draft-controls').length === 0); + assert.ok(find(".shared-draft-controls").length === 0); }); }); diff --git a/test/javascripts/acceptance/sign-in-test.js.es6 b/test/javascripts/acceptance/sign-in-test.js.es6 index 1320be56ee..0a075db20f 100644 --- a/test/javascripts/acceptance/sign-in-test.js.es6 +++ b/test/javascripts/acceptance/sign-in-test.js.es6 @@ -5,23 +5,29 @@ QUnit.test("sign in", assert => { visit("/"); click("header .login-button"); andThen(() => { - assert.ok(exists('.login-modal'), "it shows the login modal"); + assert.ok(exists(".login-modal"), "it shows the login modal"); }); // Test invalid password first - fillIn('#login-account-name', 'eviltrout'); - fillIn('#login-account-password', 'incorrect'); - click('.modal-footer .btn-primary'); + fillIn("#login-account-name", "eviltrout"); + fillIn("#login-account-password", "incorrect"); + click(".modal-footer .btn-primary"); andThen(() => { - assert.ok(exists('#modal-alert:visible'), 'it displays the login error'); - assert.not(exists('.modal-footer .btn-primary:disabled'), "enables the login button"); + assert.ok(exists("#modal-alert:visible"), "it displays the login error"); + assert.not( + exists(".modal-footer .btn-primary:disabled"), + "enables the login button" + ); }); // Use the correct password - fillIn('#login-account-password', 'correct'); - click('.modal-footer .btn-primary'); + fillIn("#login-account-password", "correct"); + click(".modal-footer .btn-primary"); andThen(() => { - assert.ok(exists('.modal-footer .btn-primary:disabled'), "disables the login button"); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "disables the login button" + ); }); }); @@ -30,21 +36,27 @@ QUnit.test("sign in - not activated", assert => { andThen(() => { click("header .login-button"); andThen(() => { - assert.ok(exists('.login-modal'), "it shows the login modal"); + assert.ok(exists(".login-modal"), "it shows the login modal"); }); - fillIn('#login-account-name', 'eviltrout'); - fillIn('#login-account-password', 'not-activated'); - click('.modal-footer .btn-primary'); + fillIn("#login-account-name", "eviltrout"); + fillIn("#login-account-password", "not-activated"); + click(".modal-footer .btn-primary"); andThen(() => { - assert.equal(find('.modal-body b').text(), 'eviltrout@example.com'); - assert.ok(!exists('.modal-body small'), 'it escapes the email address'); + assert.equal( + find(".modal-body b").text(), + "eviltrout@example.com" + ); + assert.ok(!exists(".modal-body small"), "it escapes the email address"); }); - click('.modal-footer button.resend'); + click(".modal-footer button.resend"); andThen(() => { - assert.equal(find('.modal-body b').text(), 'current@example.com'); - assert.ok(!exists('.modal-body small'), 'it escapes the email address'); + assert.equal( + find(".modal-body b").text(), + "current@example.com" + ); + assert.ok(!exists(".modal-body small"), "it escapes the email address"); }); }); }); @@ -54,24 +66,28 @@ QUnit.test("sign in - not activated - edit email", assert => { andThen(() => { click("header .login-button"); andThen(() => { - assert.ok(exists('.login-modal'), "it shows the login modal"); + assert.ok(exists(".login-modal"), "it shows the login modal"); }); - fillIn('#login-account-name', 'eviltrout'); - fillIn('#login-account-password', 'not-activated-edit'); - click('.modal-footer .btn-primary'); - click('.modal-footer button.edit-email'); + fillIn("#login-account-name", "eviltrout"); + fillIn("#login-account-password", "not-activated-edit"); + click(".modal-footer .btn-primary"); + click(".modal-footer button.edit-email"); andThen(() => { - assert.equal(find('.activate-new-email').val(), 'current@example.com'); - assert.equal(find('.modal-footer .btn-primary:disabled').length, 1, "must change email"); + assert.equal(find(".activate-new-email").val(), "current@example.com"); + assert.equal( + find(".modal-footer .btn-primary:disabled").length, + 1, + "must change email" + ); }); - fillIn('.activate-new-email', 'different@example.com'); + fillIn(".activate-new-email", "different@example.com"); andThen(() => { - assert.equal(find('.modal-footer .btn-primary:disabled').length, 0); + assert.equal(find(".modal-footer .btn-primary:disabled").length, 0); }); click(".modal-footer .btn-primary"); andThen(() => { - assert.equal(find('.modal-body b').text(), 'different@example.com'); + assert.equal(find(".modal-body b").text(), "different@example.com"); }); }); }); @@ -81,25 +97,37 @@ QUnit.test("second factor", assert => { click("header .login-button"); andThen(() => { - assert.ok(exists('.login-modal'), "it shows the login modal"); + assert.ok(exists(".login-modal"), "it shows the login modal"); }); - fillIn('#login-account-name', 'eviltrout'); - fillIn('#login-account-password', 'need-second-factor'); - click('.modal-footer .btn-primary'); + fillIn("#login-account-name", "eviltrout"); + fillIn("#login-account-password", "need-second-factor"); + click(".modal-footer .btn-primary"); andThen(() => { - assert.not(exists('#modal-alert:visible'), 'it hides the login error'); - assert.not(exists('#credentials:visible'), 'it hides the username and password prompt'); - assert.ok(exists('#second-factor:visible'), 'it displays the second factor prompt'); - assert.not(exists('.modal-footer .btn-primary:disabled'), "enables the login button"); + assert.not(exists("#modal-alert:visible"), "it hides the login error"); + assert.not( + exists("#credentials:visible"), + "it hides the username and password prompt" + ); + assert.ok( + exists("#second-factor:visible"), + "it displays the second factor prompt" + ); + assert.not( + exists(".modal-footer .btn-primary:disabled"), + "enables the login button" + ); }); - fillIn('#login-second-factor', '123456'); - click('.modal-footer .btn-primary'); + fillIn("#login-second-factor", "123456"); + click(".modal-footer .btn-primary"); andThen(() => { - assert.ok(exists('.modal-footer .btn-primary:disabled'), "disables the login button"); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "disables the login button" + ); }); }); @@ -108,29 +136,47 @@ QUnit.test("create account", assert => { click("header .sign-up-button"); andThen(() => { - assert.ok(exists('.create-account'), "it shows the create account modal"); - assert.ok(exists('.modal-footer .btn-primary:disabled'), 'create account is disabled at first'); + assert.ok(exists(".create-account"), "it shows the create account modal"); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "create account is disabled at first" + ); }); - fillIn('#new-account-name', 'Dr. Good Tuna'); - fillIn('#new-account-password', 'cool password bro'); + fillIn("#new-account-name", "Dr. Good Tuna"); + fillIn("#new-account-password", "cool password bro"); // Check username - fillIn('#new-account-email', 'good.tuna@test.com'); - fillIn('#new-account-username', 'taken'); + fillIn("#new-account-email", "good.tuna@test.com"); + fillIn("#new-account-username", "taken"); andThen(() => { - assert.ok(exists('#username-validation.bad'), 'the username validation is bad'); - assert.ok(exists('.modal-footer .btn-primary:disabled'), 'create account is still disabled'); + assert.ok( + exists("#username-validation.bad"), + "the username validation is bad" + ); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "create account is still disabled" + ); }); - fillIn('#new-account-username', 'goodtuna'); + fillIn("#new-account-username", "goodtuna"); andThen(() => { - assert.ok(exists('#username-validation.good'), 'the username validation is good'); - assert.not(exists('.modal-footer .btn-primary:disabled'), 'create account is enabled'); + assert.ok( + exists("#username-validation.good"), + "the username validation is good" + ); + assert.not( + exists(".modal-footer .btn-primary:disabled"), + "create account is enabled" + ); }); - click('.modal-footer .btn-primary'); + click(".modal-footer .btn-primary"); andThen(() => { - assert.ok(exists('.modal-footer .btn-primary:disabled'), "create account is disabled"); + assert.ok( + exists(".modal-footer .btn-primary:disabled"), + "create account is disabled" + ); }); }); diff --git a/test/javascripts/acceptance/static-test.js.es6 b/test/javascripts/acceptance/static-test.js.es6 index 97592dbd4a..47fa81dd5e 100644 --- a/test/javascripts/acceptance/static-test.js.es6 +++ b/test/javascripts/acceptance/static-test.js.es6 @@ -4,30 +4,34 @@ acceptance("Static"); QUnit.test("Static Pages", assert => { visit("/faq"); andThen(() => { - assert.ok($('body.static-faq').length, "has the body class"); + assert.ok($("body.static-faq").length, "has the body class"); assert.ok(exists(".body-page"), "The content is present"); }); visit("/guidelines"); andThen(() => { - assert.ok($('body.static-guidelines').length, "has the body class"); + assert.ok($("body.static-guidelines").length, "has the body class"); assert.ok(exists(".body-page"), "The content is present"); }); visit("/tos"); andThen(() => { - assert.ok($('body.static-tos').length, "has the body class"); + assert.ok($("body.static-tos").length, "has the body class"); assert.ok(exists(".body-page"), "The content is present"); }); visit("/privacy"); andThen(() => { - assert.ok($('body.static-privacy').length, "has the body class"); + assert.ok($("body.static-privacy").length, "has the body class"); assert.ok(exists(".body-page"), "The content is present"); }); visit("/login"); andThen(() => { - assert.equal(currentPath(), "discovery.latest", "it redirects them to latest unless `login_required`"); + assert.equal( + currentPath(), + "discovery.latest", + "it redirects them to latest unless `login_required`" + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/tag-hashtag-test.js.es6 b/test/javascripts/acceptance/tag-hashtag-test.js.es6 index 559b9ff926..11182aec80 100644 --- a/test/javascripts/acceptance/tag-hashtag-test.js.es6 +++ b/test/javascripts/acceptance/tag-hashtag-test.js.es6 @@ -4,40 +4,49 @@ acceptance("Tag Hashtag", { loggedIn: true, settings: { tagging_enabled: true }, beforeEach() { - const response = (object) => { - return [ - 200, - {"Content-Type": "application/json"}, - object - ]; + const response = object => { + return [200, { "Content-Type": "application/json" }, object]; }; - server.get('/tags/filter/search', () => { //eslint-disable-line - return response({ results: [{ text: 'monkey', count: 1 }] }); + // prettier-ignore + server.get("/tags/filter/search", () => { //eslint-disable-line + return response({ results: [{ text: "monkey", count: 1 }] }); }); - server.get('/category_hashtags/check', () => { //eslint-disable-line + // prettier-ignore + server.get("/category_hashtags/check", () => { //eslint-disable-line return response({ valid: [] }); }); - server.get('/tags/check', () => { //eslint-disable-line - return response({ valid: [{ value: 'monkey', url: '/tags/monkey' }] }); + // prettier-ignore + server.get("/tags/check", () => { //eslint-disable-line + return response({ valid: [{ value: "monkey", url: "/tags/monkey" }] }); }); } }); QUnit.test("tag is cooked properly", assert => { visit("/t/internationalization-localization/280"); - click('#topic-footer-buttons .btn.create'); + click("#topic-footer-buttons .btn.create"); - fillIn('.d-editor-input', "this is a tag hashtag #monkey::tag"); + fillIn(".d-editor-input", "this is a tag hashtag #monkey::tag"); andThen(() => { // TODO: Test that the autocomplete shows - assert.equal(find('.d-editor-preview:visible').html().trim(), "

this is a tag hashtag #monkey

"); + assert.equal( + find(".d-editor-preview:visible") + .html() + .trim(), + '

this is a tag hashtag #monkey

' + ); }); - click('#reply-control .btn.create'); + click("#reply-control .btn.create"); andThen(() => { - assert.equal(find('.topic-post:last .cooked').html().trim(), "

this is a tag hashtag #monkey

"); + assert.equal( + find(".topic-post:last .cooked") + .html() + .trim(), + '

this is a tag hashtag #monkey

' + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/tags-test.js.es6 b/test/javascripts/acceptance/tags-test.js.es6 index 8971f83ee3..5052ebd0b3 100644 --- a/test/javascripts/acceptance/tags-test.js.es6 +++ b/test/javascripts/acceptance/tags-test.js.es6 @@ -5,8 +5,8 @@ QUnit.test("list the tags", assert => { visit("/tags"); andThen(() => { - assert.ok($('body.tags-page').length, "has the body class"); - assert.ok(exists('.tag-eviltrout'), "shows the evil trout tag"); + assert.ok($("body.tags-page").length, "has the body class"); + assert.ok(exists(".tag-eviltrout"), "shows the evil trout tag"); }); }); @@ -17,48 +17,70 @@ acceptance("Tags listed by group", { } }); - QUnit.test("list the tags in groups", assert => { - server.get('/tags', () => { // eslint-disable-line no-undef + // prettier-ignore + server.get("/tags", () => { // eslint-disable-line no-undef return [ 200, { "Content-Type": "application/json" }, { - "tags":[{id: 'planned', text: 'planned', count: 7, pm_count: 0}], - "extras": { "tag_groups": [ - {id: 2, name: "Ford Cars", tags: [ - {id: 'escort', text: 'escort', count: 1, pm_count: 0}, - {id: 'focus', text: 'focus', count: 3, pm_count: 0} - ]}, - {id: 1, name: "Honda Cars", tags: [ - {id: 'civic', text: 'civic', count: 4, pm_count: 0}, - {id: 'accord', text: 'accord', count: 2, pm_count: 0} - ]}, - {id: 1, name: "Makes", tags: [ - {id: 'ford', text: 'ford', count: 5, pm_count: 0}, - {id: 'honda', text: 'honda', count: 6, pm_count: 0} - ]} - ]} + tags: [{ id: "planned", text: "planned", count: 7, pm_count: 0 }], + extras: { + tag_groups: [ + { + id: 2, + name: "Ford Cars", + tags: [ + { id: "escort", text: "escort", count: 1, pm_count: 0 }, + { id: "focus", text: "focus", count: 3, pm_count: 0 } + ] + }, + { + id: 1, + name: "Honda Cars", + tags: [ + { id: "civic", text: "civic", count: 4, pm_count: 0 }, + { id: "accord", text: "accord", count: 2, pm_count: 0 } + ] + }, + { + id: 1, + name: "Makes", + tags: [ + { id: "ford", text: "ford", count: 5, pm_count: 0 }, + { id: "honda", text: "honda", count: 6, pm_count: 0 } + ] + } + ] + } } ]; }); - visit('/tags'); + visit("/tags"); andThen(() => { - assert.equal($('.tag-list').length, 4, "shows separate lists for the 3 groups and the ungrouped tags"); - assert.ok( - _.isEqual( - _.map($('.tag-list h3'), i => { return $(i).text(); } ), - ['Ford Cars', 'Honda Cars', 'Makes', 'Other Tags'] - ), - 'shown in given order and with tags that are not in a group' + assert.equal( + $(".tag-list").length, + 4, + "shows separate lists for the 3 groups and the ungrouped tags" ); assert.ok( _.isEqual( - _.map($('.tag-list:first .discourse-tag'), i => { return $(i).text(); }), - ['focus', 'escort'] + _.map($(".tag-list h3"), i => { + return $(i).text(); + }), + ["Ford Cars", "Honda Cars", "Makes", "Other Tags"] ), - 'shows the tags in default sort (by count)' + "shown in given order and with tags that are not in a group" + ); + assert.ok( + _.isEqual( + _.map($(".tag-list:first .discourse-tag"), i => { + return $(i).text(); + }), + ["focus", "escort"] + ), + "shows the tags in default sort (by count)" ); }); }); diff --git a/test/javascripts/acceptance/topic-anonymous-test.js.es6 b/test/javascripts/acceptance/topic-anonymous-test.js.es6 index c0eff19865..1ec1ed32f5 100644 --- a/test/javascripts/acceptance/topic-anonymous-test.js.es6 +++ b/test/javascripts/acceptance/topic-anonymous-test.js.es6 @@ -6,7 +6,10 @@ QUnit.test("Enter a Topic", assert => { andThen(() => { assert.ok(exists("#topic"), "The topic was rendered"); assert.ok(exists("#topic .cooked"), "The topic has cooked posts"); - assert.ok(find('.shared-draft-notice').length === 0, "no shared draft unless there's a dest category id"); + assert.ok( + find(".shared-draft-notice").length === 0, + "no shared draft unless there's a dest category id" + ); }); }); @@ -21,7 +24,10 @@ QUnit.test("Enter a 404 topic", assert => { visit("/t/not-found/404"); andThen(() => { assert.ok(!exists("#topic"), "The topic was not rendered"); - assert.ok(find(".not-found").text() === "not found", "it renders the error message"); + assert.ok( + find(".not-found").text() === "not found", + "it renders the error message" + ); }); }); diff --git a/test/javascripts/acceptance/topic-discovery-test.js.es6 b/test/javascripts/acceptance/topic-discovery-test.js.es6 index 458c3471a0..e66382be96 100644 --- a/test/javascripts/acceptance/topic-discovery-test.js.es6 +++ b/test/javascripts/acceptance/topic-discovery-test.js.es6 @@ -4,43 +4,64 @@ acceptance("Topic Discovery"); QUnit.test("Visit Discovery Pages", assert => { visit("/"); andThen(() => { - assert.ok($('body.navigation-topics').length, "has the default navigation"); + assert.ok($("body.navigation-topics").length, "has the default navigation"); assert.ok(exists(".topic-list"), "The list of topics was rendered"); - assert.ok(exists('.topic-list .topic-list-item'), "has topics"); + assert.ok(exists(".topic-list .topic-list-item"), "has topics"); }); visit("/c/bug"); andThen(() => { assert.ok(exists(".topic-list"), "The list of topics was rendered"); - assert.ok(exists('.topic-list .topic-list-item'), "has topics"); - assert.ok(!exists('.category-list'), "doesn't render subcategories"); - assert.ok($('body.category-bug').length, "has a custom css class for the category id on the body"); + assert.ok(exists(".topic-list .topic-list-item"), "has topics"); + assert.ok(!exists(".category-list"), "doesn't render subcategories"); + assert.ok( + $("body.category-bug").length, + "has a custom css class for the category id on the body" + ); }); visit("/categories"); andThen(() => { - assert.ok($('body.navigation-categories').length, "has the body class"); - assert.ok($('body.category-bug').length === 0, "removes the custom category class"); - assert.ok(exists('.category'), "has a list of categories"); - assert.ok($('body.categories-list').length, "has a custom class to indicate categories"); + assert.ok($("body.navigation-categories").length, "has the body class"); + assert.ok( + $("body.category-bug").length === 0, + "removes the custom category class" + ); + assert.ok(exists(".category"), "has a list of categories"); + assert.ok( + $("body.categories-list").length, + "has a custom class to indicate categories" + ); }); visit("/top"); andThen(() => { - assert.ok($('body.categories-list').length === 0, "removes the `categories-list` class"); - assert.ok(exists('.topic-list .topic-list-item'), "has topics"); + assert.ok( + $("body.categories-list").length === 0, + "removes the `categories-list` class" + ); + assert.ok(exists(".topic-list .topic-list-item"), "has topics"); }); visit("/c/feature"); andThen(() => { assert.ok(exists(".topic-list"), "The list of topics was rendered"); - assert.ok(exists(".category-boxes"), "The list of subcategories were rendered with box style"); + assert.ok( + exists(".category-boxes"), + "The list of subcategories were rendered with box style" + ); }); visit("/c/dev"); andThen(() => { assert.ok(exists(".topic-list"), "The list of topics was rendered"); - assert.ok(exists(".category-boxes-with-topics"), "The list of subcategories were rendered with box-with-featured-topics style"); - assert.ok(exists(".category-boxes-with-topics .featured-topics"), "The featured topics are there too"); + assert.ok( + exists(".category-boxes-with-topics"), + "The list of subcategories were rendered with box-with-featured-topics style" + ); + assert.ok( + exists(".category-boxes-with-topics .featured-topics"), + "The featured topics are there too" + ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/topic-edit-timer-test.js.es6 b/test/javascripts/acceptance/topic-edit-timer-test.js.es6 index b3ed4df744..f437f29884 100644 --- a/test/javascripts/acceptance/topic-edit-timer-test.js.es6 +++ b/test/javascripts/acceptance/topic-edit-timer-test.js.es6 @@ -1,163 +1,207 @@ -import { acceptance, replaceCurrentUser } from 'helpers/qunit-helpers'; -acceptance('Topic - Edit timer', { loggedIn: true }); +import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers"; +acceptance("Topic - Edit timer", { loggedIn: true }); -QUnit.test('default', assert => { - const timerType = selectKit('.select-kit.timer-type'); - const futureDateInputSelector = selectKit('.future-date-input-selector'); +QUnit.test("default", assert => { + const timerType = selectKit(".select-kit.timer-type"); + const futureDateInputSelector = selectKit(".future-date-input-selector"); - visit('/t/internationalization-localization'); - click('.toggle-admin-menu'); - click('.topic-admin-status-update button'); + visit("/t/internationalization-localization"); + click(".toggle-admin-menu"); + click(".topic-admin-status-update button"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal( + futureDateInputSelector.header().title(), + "Select a timeframe" + ); assert.equal(futureDateInputSelector.header().value(), null); }); - click('#private-topic-timer'); + click("#private-topic-timer"); andThen(() => { - assert.equal(timerType.header().title(), 'Remind Me'); - assert.equal(timerType.header().value(), 'reminder'); + assert.equal(timerType.header().title(), "Remind Me"); + assert.equal(timerType.header().value(), "reminder"); - assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal( + futureDateInputSelector.header().title(), + "Select a timeframe" + ); assert.equal(futureDateInputSelector.header().value(), null); }); }); -QUnit.test('autoclose - specific time', assert => { - const futureDateInputSelector = selectKit('.future-date-input-selector'); +QUnit.test("autoclose - specific time", assert => { + const futureDateInputSelector = selectKit(".future-date-input-selector"); - visit('/t/internationalization-localization'); - click('.toggle-admin-menu'); - click('.topic-admin-status-update button'); + visit("/t/internationalization-localization"); + click(".toggle-admin-menu"); + click(".topic-admin-status-update button"); - futureDateInputSelector.expand().selectRowByValue('next_week'); + futureDateInputSelector.expand().selectRowByValue("next_week"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Next week'); - assert.equal(futureDateInputSelector.header().value(), 'next_week'); + assert.equal(futureDateInputSelector.header().title(), "Next week"); + assert.equal(futureDateInputSelector.header().value(), "next_week"); const regex = /will automatically close in/g; - const html = find('.future-date-input .topic-status-info').html().trim(); + const html = find(".future-date-input .topic-status-info") + .html() + .trim(); assert.ok(regex.test(html)); }); }); -QUnit.test('autoclose', assert => { - const futureDateInputSelector = selectKit('.future-date-input-selector'); +QUnit.test("autoclose", assert => { + const futureDateInputSelector = selectKit(".future-date-input-selector"); - visit('/t/internationalization-localization'); - click('.toggle-admin-menu'); - click('.topic-admin-status-update button'); + visit("/t/internationalization-localization"); + click(".toggle-admin-menu"); + click(".topic-admin-status-update button"); - futureDateInputSelector.expand().selectRowByValue('next_week'); + futureDateInputSelector.expand().selectRowByValue("next_week"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Next week'); - assert.equal(futureDateInputSelector.header().value(), 'next_week'); + assert.equal(futureDateInputSelector.header().title(), "Next week"); + assert.equal(futureDateInputSelector.header().value(), "next_week"); const regex = /will automatically close in/g; - const html = find('.future-date-input .topic-status-info').html().trim(); + const html = find(".future-date-input .topic-status-info") + .html() + .trim(); assert.ok(regex.test(html)); }); - futureDateInputSelector.expand().selectRowByValue('pick_date_and_time'); + futureDateInputSelector.expand().selectRowByValue("pick_date_and_time"); - fillIn('.future-date-input .date-picker', '2099-11-24'); + fillIn(".future-date-input .date-picker", "2099-11-24"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Pick date and time'); - assert.equal(futureDateInputSelector.header().value(), 'pick_date_and_time'); + assert.equal( + futureDateInputSelector.header().title(), + "Pick date and time" + ); + assert.equal( + futureDateInputSelector.header().value(), + "pick_date_and_time" + ); const regex = /will automatically close in/g; - const html = find('.future-date-input .topic-status-info').html().trim(); + const html = find(".future-date-input .topic-status-info") + .html() + .trim(); assert.ok(regex.test(html)); }); - futureDateInputSelector.expand().selectRowByValue('set_based_on_last_post'); + futureDateInputSelector.expand().selectRowByValue("set_based_on_last_post"); - fillIn('.future-date-input input[type=number]', '2'); + fillIn(".future-date-input input[type=number]", "2"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Close based on last post'); - assert.equal(futureDateInputSelector.header().value(), 'set_based_on_last_post'); + assert.equal( + futureDateInputSelector.header().title(), + "Close based on last post" + ); + assert.equal( + futureDateInputSelector.header().value(), + "set_based_on_last_post" + ); const regex = /This topic will close.*after the last reply/g; - const html = find('.future-date-input .topic-status-info').html().trim(); + const html = find(".future-date-input .topic-status-info") + .html() + .trim(); assert.ok(regex.test(html)); }); }); -QUnit.test('close temporarily', assert => { - const timerType = selectKit('.select-kit.timer-type'); - const futureDateInputSelector = selectKit('.future-date-input-selector'); +QUnit.test("close temporarily", assert => { + const timerType = selectKit(".select-kit.timer-type"); + const futureDateInputSelector = selectKit(".future-date-input-selector"); - visit('/t/internationalization-localization'); - click('.toggle-admin-menu'); - click('.topic-admin-status-update button'); + visit("/t/internationalization-localization"); + click(".toggle-admin-menu"); + click(".topic-admin-status-update button"); - timerType.expand().selectRowByValue('open'); + timerType.expand().selectRowByValue("open"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal( + futureDateInputSelector.header().title(), + "Select a timeframe" + ); assert.equal(futureDateInputSelector.header().value(), null); }); - futureDateInputSelector.expand().selectRowByValue('next_week'); + futureDateInputSelector.expand().selectRowByValue("next_week"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Next week'); - assert.equal(futureDateInputSelector.header().value(), 'next_week'); + assert.equal(futureDateInputSelector.header().title(), "Next week"); + assert.equal(futureDateInputSelector.header().value(), "next_week"); const regex = /will automatically open in/g; - const html = find('.future-date-input .topic-status-info').html().trim(); + const html = find(".future-date-input .topic-status-info") + .html() + .trim(); assert.ok(regex.test(html)); }); - futureDateInputSelector.expand().selectRowByValue('pick_date_and_time'); + futureDateInputSelector.expand().selectRowByValue("pick_date_and_time"); - fillIn('.future-date-input .date-picker', '2099-11-24'); + fillIn(".future-date-input .date-picker", "2099-11-24"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Pick date and time'); - assert.equal(futureDateInputSelector.header().value(), 'pick_date_and_time'); + assert.equal( + futureDateInputSelector.header().title(), + "Pick date and time" + ); + assert.equal( + futureDateInputSelector.header().value(), + "pick_date_and_time" + ); const regex = /will automatically open in/g; - const html = find('.future-date-input .topic-status-info').html().trim(); + const html = find(".future-date-input .topic-status-info") + .html() + .trim(); assert.ok(regex.test(html)); }); }); -QUnit.test('schedule', assert => { - const timerType = selectKit('.select-kit.timer-type'); - const categoryChooser = selectKit('.modal-body .category-chooser'); - const futureDateInputSelector = selectKit('.future-date-input-selector'); +QUnit.test("schedule", assert => { + const timerType = selectKit(".select-kit.timer-type"); + const categoryChooser = selectKit(".modal-body .category-chooser"); + const futureDateInputSelector = selectKit(".future-date-input-selector"); - visit('/t/internationalization-localization'); - click('.toggle-admin-menu'); - click('.topic-admin-status-update button'); + visit("/t/internationalization-localization"); + click(".toggle-admin-menu"); + click(".topic-admin-status-update button"); - timerType.expand().selectRowByValue('publish_to_category'); + timerType.expand().selectRowByValue("publish_to_category"); andThen(() => { - assert.equal(categoryChooser.header().title(), 'uncategorized'); + assert.equal(categoryChooser.header().title(), "uncategorized"); assert.equal(categoryChooser.header().value(), null); - assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal( + futureDateInputSelector.header().title(), + "Select a timeframe" + ); assert.equal(futureDateInputSelector.header().value(), null); }); - categoryChooser.expand().selectRowByValue('7'); + categoryChooser.expand().selectRowByValue("7"); - futureDateInputSelector.expand().selectRowByValue('next_week'); + futureDateInputSelector.expand().selectRowByValue("next_week"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Next week'); - assert.equal(futureDateInputSelector.header().value(), 'next_week'); + assert.equal(futureDateInputSelector.header().title(), "Next week"); + assert.equal(futureDateInputSelector.header().value(), "next_week"); const regex = /will be published to #dev/g; - const text = find('.future-date-input .topic-status-info').text().trim(); + const text = find(".future-date-input .topic-status-info") + .text() + .trim(); assert.ok(regex.test(text)); }); }); @@ -165,11 +209,11 @@ QUnit.test('schedule', assert => { QUnit.test("TL4 can't auto-delete", assert => { replaceCurrentUser({ staff: false, trust_level: 4 }); - visit('/t/internationalization-localization'); - click('.toggle-admin-menu'); - click('.topic-admin-status-update button'); + visit("/t/internationalization-localization"); + click(".toggle-admin-menu"); + click(".topic-admin-status-update button"); - const timerType = selectKit('.select-kit.timer-type'); + const timerType = selectKit(".select-kit.timer-type"); timerType.expand(); @@ -178,29 +222,34 @@ QUnit.test("TL4 can't auto-delete", assert => { }); }); -QUnit.test('auto delete', assert => { - const timerType = selectKit('.select-kit.timer-type'); - const futureDateInputSelector = selectKit('.future-date-input-selector'); +QUnit.test("auto delete", assert => { + const timerType = selectKit(".select-kit.timer-type"); + const futureDateInputSelector = selectKit(".future-date-input-selector"); - visit('/t/internationalization-localization'); - click('.toggle-admin-menu'); - click('.topic-admin-status-update button'); + visit("/t/internationalization-localization"); + click(".toggle-admin-menu"); + click(".topic-admin-status-update button"); - timerType.expand().selectRowByValue('delete'); + timerType.expand().selectRowByValue("delete"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Select a timeframe'); + assert.equal( + futureDateInputSelector.header().title(), + "Select a timeframe" + ); assert.equal(futureDateInputSelector.header().value(), null); }); - futureDateInputSelector.expand().selectRowByValue('two_weeks'); + futureDateInputSelector.expand().selectRowByValue("two_weeks"); andThen(() => { - assert.equal(futureDateInputSelector.header().title(), 'Two Weeks'); - assert.equal(futureDateInputSelector.header().value(), 'two_weeks'); + assert.equal(futureDateInputSelector.header().title(), "Two Weeks"); + assert.equal(futureDateInputSelector.header().value(), "two_weeks"); const regex = /will be automatically deleted/g; - const html = find('.future-date-input .topic-status-info').html().trim(); + const html = find(".future-date-input .topic-status-info") + .html() + .trim(); assert.ok(regex.test(html)); }); }); diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 index 4fe8877595..7b38d69455 100644 --- a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -3,21 +3,20 @@ acceptance("Topic Notifications button", { loggedIn: true, beforeEach() { const response = object => { - return [ - 200, - { "Content-Type": "application/json" }, - object - ]; + return [200, { "Content-Type": "application/json" }, object]; }; - server.post('/t/280/notifications', () => { // eslint-disable-line no-undef + // prettier-ignore + server.post("/t/280/notifications", () => { // eslint-disable-line no-undef return response({}); }); } }); QUnit.test("Updating topic notification level", assert => { - const notificationOptions = selectKit("#topic-footer-buttons .topic-notifications-options"); + const notificationOptions = selectKit( + "#topic-footer-buttons .topic-notifications-options" + ); visit("/t/internationalization-localization/280"); diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6 index b84afe844f..4d5e935a97 100644 --- a/test/javascripts/acceptance/topic-test.js.es6 +++ b/test/javascripts/acceptance/topic-test.js.es6 @@ -4,17 +4,17 @@ acceptance("Topic", { loggedIn: true }); QUnit.test("Share Popup", assert => { visit("/t/internationalization-localization/280"); andThen(() => { - assert.ok(!exists('#share-link.visible'), 'it is not visible'); + assert.ok(!exists("#share-link.visible"), "it is not visible"); }); click("button[data-share-url]"); andThen(() => { - assert.ok(exists('#share-link.visible'), 'it shows the popup'); + assert.ok(exists("#share-link.visible"), "it shows the popup"); }); - click('#share-link .close-share'); + click("#share-link .close-share"); andThen(() => { - assert.ok(!exists('#share-link.visible'), 'it closes the popup'); + assert.ok(!exists("#share-link.visible"), "it closes the popup"); }); // TODO tgxworld This fails on Travis but we need to push the security fix out @@ -33,57 +33,67 @@ QUnit.test("Share Popup", assert => { QUnit.test("Showing and hiding the edit controls", assert => { visit("/t/internationalization-localization/280"); - click('#topic-title .d-icon-pencil'); + click("#topic-title .d-icon-pencil"); andThen(() => { - assert.ok(exists('#edit-title'), 'it shows the editing controls'); - assert.ok(!exists('.title-wrapper .remove-featured-link'), 'link to remove featured link is not shown'); + assert.ok(exists("#edit-title"), "it shows the editing controls"); + assert.ok( + !exists(".title-wrapper .remove-featured-link"), + "link to remove featured link is not shown" + ); }); - fillIn('#edit-title', 'this is the new title'); - click('#topic-title .cancel-edit'); + fillIn("#edit-title", "this is the new title"); + click("#topic-title .cancel-edit"); andThen(() => { - assert.ok(!exists('#edit-title'), 'it hides the editing controls'); + assert.ok(!exists("#edit-title"), "it hides the editing controls"); }); }); QUnit.test("Updating the topic title and category", assert => { - const categoryChooser = selectKit('.title-wrapper .category-chooser'); + const categoryChooser = selectKit(".title-wrapper .category-chooser"); visit("/t/internationalization-localization/280"); - click('#topic-title .d-icon-pencil'); - fillIn('#edit-title', 'this is the new title'); + click("#topic-title .d-icon-pencil"); + fillIn("#edit-title", "this is the new title"); categoryChooser.expand().selectRowByValue(4); - click('#topic-title .submit-edit'); + click("#topic-title .submit-edit"); andThen(() => { - assert.equal(find('#topic-title .badge-category').text(), 'faq', 'it displays the new category'); - assert.equal(find('.fancy-title').text().trim(), 'this is the new title', 'it displays the new title'); + assert.equal( + find("#topic-title .badge-category").text(), + "faq", + "it displays the new category" + ); + assert.equal( + find(".fancy-title") + .text() + .trim(), + "this is the new title", + "it displays the new title" + ); }); }); QUnit.test("Marking a topic as wiki", assert => { - server.put('/posts/398/wiki', () => { // eslint-disable-line no-undef - return [ - 200, - { "Content-Type": "application/json" }, - {} - ]; + // prettier-ignore + server.put("/posts/398/wiki", () => { // eslint-disable-line no-undef + return [200, { "Content-Type": "application/json" }, {}]; }); visit("/t/internationalization-localization/280"); andThen(() => { - assert.ok(find('a.wiki').length === 0, 'it does not show the wiki icon'); + assert.ok(find("a.wiki").length === 0, "it does not show the wiki icon"); }); - click('.topic-post:eq(0) button.show-more-actions'); - click('.topic-post:eq(0) button.show-post-admin-menu'); - click('.btn.wiki'); + click(".topic-post:eq(0) button.show-more-actions"); + click(".topic-post:eq(0) button.show-post-admin-menu"); + click(".btn.wiki"); andThen(() => { - assert.ok(find('a.wiki').length === 1, 'it shows the wiki icon'); + assert.ok(find("a.wiki").length === 1, "it shows the wiki icon"); }); }); @@ -93,15 +103,22 @@ QUnit.test("Reply as new topic", assert => { click(".reply-as-new-topic a"); andThen(() => { - assert.ok(exists('.d-editor-input'), 'the composer input is visible'); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); assert.equal( - find('.d-editor-input').val().trim(), - `Continuing the discussion from [Internationalization / localization](${window.location.origin}/t/internationalization-localization/280):`, + find(".d-editor-input") + .val() + .trim(), + `Continuing the discussion from [Internationalization / localization](${ + window.location.origin + }/t/internationalization-localization/280):`, "it fills composer with the ring string" ); assert.equal( - selectKit('.category-chooser').header().value(), "2", + selectKit(".category-chooser") + .header() + .value(), + "2", "it fills category selector with the right category" ); }); @@ -113,28 +130,35 @@ QUnit.test("Reply as new message", assert => { click(".reply-as-new-topic a"); andThen(() => { - assert.ok(exists('.d-editor-input'), 'the composer input is visible'); + assert.ok(exists(".d-editor-input"), "the composer input is visible"); assert.equal( - find('.d-editor-input').val().trim(), - `Continuing the discussion from [PM for testing](${window.location.origin}/t/pm-for-testing/12):`, + find(".d-editor-input") + .val() + .trim(), + `Continuing the discussion from [PM for testing](${ + window.location.origin + }/t/pm-for-testing/12):`, "it fills composer with the ring string" ); - const targets = find('.item span', '.composer-fields'); + const targets = find(".item span", ".composer-fields"); assert.equal( - $(targets[0]).text(), "someguy", + $(targets[0]).text(), + "someguy", "it fills up the composer with the right user to start the PM to" ); assert.equal( - $(targets[1]).text(), "test", + $(targets[1]).text(), + "test", "it fills up the composer with the right user to start the PM to" ); assert.equal( - $(targets[2]).text(), "Group", + $(targets[2]).text(), + "Group", "it fills up the composer with the right group to start the PM to" ); }); @@ -145,8 +169,11 @@ QUnit.test("Visit topic routes", assert => { andThen(() => { assert.equal( - find('.fancy-title').text().trim(), 'PM for testing', - 'it routes to the right topic' + find(".fancy-title") + .text() + .trim(), + "PM for testing", + "it routes to the right topic" ); }); @@ -154,22 +181,31 @@ QUnit.test("Visit topic routes", assert => { andThen(() => { assert.equal( - find('.fancy-title').text().trim(), 'Internationalization / localization', - 'it routes to the right topic' + find(".fancy-title") + .text() + .trim(), + "Internationalization / localization", + "it routes to the right topic" ); }); }); QUnit.test("Updating the topic title with emojis", assert => { visit("/t/internationalization-localization/280"); - click('#topic-title .d-icon-pencil'); + click("#topic-title .d-icon-pencil"); - fillIn('#edit-title', 'emojis title :bike: :blonde_woman:t6:'); + fillIn("#edit-title", "emojis title :bike: :blonde_woman:t6:"); - click('#topic-title .submit-edit'); + click("#topic-title .submit-edit"); andThen(() => { - assert.equal(find('.fancy-title').html().trim(), 'emojis title \"bike\" \"blonde_woman:t6\"', 'it displays the new title with emojis'); + assert.equal( + find(".fancy-title") + .html() + .trim(), + 'emojis title bike blonde_woman:t6', + "it displays the new title with emojis" + ); }); }); @@ -184,12 +220,18 @@ acceptance("Topic featured links", { QUnit.test("remove featured link", assert => { visit("/t/299/1"); andThen(() => { - assert.ok(exists('.title-wrapper .topic-featured-link'), 'link is shown with topic title'); + assert.ok( + exists(".title-wrapper .topic-featured-link"), + "link is shown with topic title" + ); }); - click('.title-wrapper .edit-topic'); + click(".title-wrapper .edit-topic"); andThen(() => { - assert.ok(exists('.title-wrapper .remove-featured-link'), 'link to remove featured link'); + assert.ok( + exists(".title-wrapper .remove-featured-link"), + "link to remove featured link" + ); }); // this test only works in a browser: diff --git a/test/javascripts/acceptance/user-anonymous-test.js.es6 b/test/javascripts/acceptance/user-anonymous-test.js.es6 index 91f3c62c75..30d8554811 100644 --- a/test/javascripts/acceptance/user-anonymous-test.js.es6 +++ b/test/javascripts/acceptance/user-anonymous-test.js.es6 @@ -3,30 +3,30 @@ acceptance("User Anonymous"); function hasStream(assert) { andThen(() => { - assert.ok(exists('.user-main .about'), 'it has the about section'); - assert.ok(count('.user-stream .item') > 0, 'it has stream items'); + assert.ok(exists(".user-main .about"), "it has the about section"); + assert.ok(count(".user-stream .item") > 0, "it has stream items"); }); } function hasTopicList(assert) { andThen(() => { - assert.equal(count('.user-stream .item'), 0, "has no stream displayed"); - assert.ok(count('.topic-list tr') > 0, 'it has a topic list'); + assert.equal(count(".user-stream .item"), 0, "has no stream displayed"); + assert.ok(count(".topic-list tr") > 0, "it has a topic list"); }); } QUnit.test("Root URL", assert => { visit("/u/eviltrout"); andThen(() => { - assert.ok($('body.user-summary-page').length, "has the body class"); - assert.equal(currentPath(), 'user.summary', "it defaults to summary"); + assert.ok($("body.user-summary-page").length, "has the body class"); + assert.equal(currentPath(), "user.summary", "it defaults to summary"); }); }); QUnit.test("Filters", assert => { visit("/u/eviltrout/activity"); andThen(() => { - assert.ok($('body.user-activity-page').length, "has the body class"); + assert.ok($("body.user-activity-page").length, "has the body class"); }); hasStream(assert); @@ -40,7 +40,7 @@ QUnit.test("Filters", assert => { QUnit.test("Badges", assert => { visit("/u/eviltrout/badges"); andThen(() => { - assert.ok($('body.user-badges-page').length, "has the body class"); + assert.ok($("body.user-badges-page").length, "has the body class"); assert.ok(exists(".user-badges-list .badge-card"), "shows a badge"); }); }); @@ -49,6 +49,10 @@ QUnit.test("Restricted Routes", assert => { visit("/u/eviltrout/preferences"); andThen(() => { - assert.equal(currentURL(), '/u/eviltrout/activity', "it redirects from preferences"); + assert.equal( + currentURL(), + "/u/eviltrout/activity", + "it redirects from preferences" + ); }); }); diff --git a/test/javascripts/acceptance/user-card-test.js.es6 b/test/javascripts/acceptance/user-card-test.js.es6 index dcab0c2930..a502f488cf 100644 --- a/test/javascripts/acceptance/user-card-test.js.es6 +++ b/test/javascripts/acceptance/user-card-test.js.es6 @@ -3,13 +3,12 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("User Card"); QUnit.test("user card", assert => { - visit('/'); + visit("/"); - assert.ok(invisible('#user-card'), 'user card is invisible by default'); - click('a[data-user-card=eviltrout]:first'); + assert.ok(invisible("#user-card"), "user card is invisible by default"); + click("a[data-user-card=eviltrout]:first"); andThen(() => { - assert.ok(visible('#user-card'), 'card should appear'); + assert.ok(visible("#user-card"), "card should appear"); }); - }); diff --git a/test/javascripts/acceptance/user-test.js.es6 b/test/javascripts/acceptance/user-test.js.es6 index 261a9b6632..05fe5885aa 100644 --- a/test/javascripts/acceptance/user-test.js.es6 +++ b/test/javascripts/acceptance/user-test.js.es6 @@ -1,46 +1,50 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("User", {loggedIn: true}); +acceptance("User", { loggedIn: true }); QUnit.test("Invites", assert => { visit("/u/eviltrout/invited/pending"); andThen(() => { - assert.ok($('body.user-invites-page').length, "has the body class"); + assert.ok($("body.user-invites-page").length, "has the body class"); }); }); QUnit.test("Messages", assert => { visit("/u/eviltrout/messages"); andThen(() => { - assert.ok($('body.user-messages-page').length, "has the body class"); + assert.ok($("body.user-messages-page").length, "has the body class"); }); }); QUnit.test("Notifications", assert => { visit("/u/eviltrout/notifications"); andThen(() => { - assert.ok($('body.user-notifications-page').length, "has the body class"); + assert.ok($("body.user-notifications-page").length, "has the body class"); }); }); QUnit.test("Root URL - Viewing Self", assert => { visit("/u/eviltrout"); andThen(() => { - assert.ok($('body.user-activity-page').length, "has the body class"); - assert.equal(currentPath(), 'user.userActivity.index', "it defaults to activity"); - assert.ok(exists('.container.viewing-self'), "has the viewing-self class"); + assert.ok($("body.user-activity-page").length, "has the body class"); + assert.equal( + currentPath(), + "user.userActivity.index", + "it defaults to activity" + ); + assert.ok(exists(".container.viewing-self"), "has the viewing-self class"); }); }); QUnit.test("Viewing Summary", assert => { visit("/u/eviltrout/summary"); andThen(() => { - assert.ok(exists('.replies-section li a'), 'replies'); - assert.ok(exists('.topics-section li a'), 'topics'); - assert.ok(exists('.links-section li a'), 'links'); - assert.ok(exists('.replied-section .user-info'), 'liked by'); - assert.ok(exists('.liked-by-section .user-info'), 'liked by'); - assert.ok(exists('.liked-section .user-info'), 'liked'); - assert.ok(exists('.badges-section .badge-card'), 'badges'); + assert.ok(exists(".replies-section li a"), "replies"); + assert.ok(exists(".topics-section li a"), "topics"); + assert.ok(exists(".links-section li a"), "links"); + assert.ok(exists(".replied-section .user-info"), "liked by"); + assert.ok(exists(".liked-by-section .user-info"), "liked by"); + assert.ok(exists(".liked-section .user-info"), "liked"); + assert.ok(exists(".badges-section .badge-card"), "badges"); }); }); diff --git a/test/javascripts/acceptance/users-test.js.es6 b/test/javascripts/acceptance/users-test.js.es6 index 648b41890d..57e1302b63 100644 --- a/test/javascripts/acceptance/users-test.js.es6 +++ b/test/javascripts/acceptance/users-test.js.es6 @@ -3,23 +3,23 @@ acceptance("User Directory"); QUnit.test("Visit Page", async assert => { await visit("/u"); - assert.ok($('body.users-page').length, "has the body class"); - assert.ok(exists('.directory table tr'), "has a list of users"); + assert.ok($("body.users-page").length, "has the body class"); + assert.ok(exists(".directory table tr"), "has a list of users"); }); QUnit.test("Visit All Time", async assert => { await visit("/u?period=all"); - assert.ok(exists('.time-read'), "has time read column"); + assert.ok(exists(".time-read"), "has time read column"); }); QUnit.test("Visit Without Usernames", async assert => { await visit("/u?exclude_usernames=system"); - assert.ok($('body.users-page').length, "has the body class"); - assert.ok(exists('.directory table tr'), "has a list of users"); + assert.ok($("body.users-page").length, "has the body class"); + assert.ok(exists(".directory table tr"), "has a list of users"); }); QUnit.test("Visit With Group Filter", async assert => { await visit("/u?group=trust_level_0"); - assert.ok($('body.users-page').length, "has the body class"); - assert.ok(exists('.directory table tr'), "has a list of users"); -}); \ No newline at end of file + assert.ok($("body.users-page").length, "has the body class"); + assert.ok(exists(".directory table tr"), "has a list of users"); +}); diff --git a/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 b/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 index 4146897093..2611c4b1c7 100644 --- a/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 +++ b/test/javascripts/admin/controllers/admin-customize-themes-test.js.es6 @@ -1,32 +1,32 @@ -import { mapRoutes } from 'discourse/mapping-router'; -import Theme from 'admin/models/theme'; +import { mapRoutes } from "discourse/mapping-router"; +import Theme from "admin/models/theme"; -moduleFor('controller:admin-customize-themes', { +moduleFor("controller:admin-customize-themes", { beforeEach() { - this.registry.register('router:main', mapRoutes()); + this.registry.register("router:main", mapRoutes()); }, - needs: ['controller:adminUser'] + needs: ["controller:adminUser"] }); QUnit.test("can list sorted themes", function(assert) { - - const defaultTheme = Theme.create({id: 2, 'default': true, name: 'default'}); - const userTheme = Theme.create({id: 3, 'user_selectable': true, name: 'name'}); - const strayTheme1 = Theme.create({id: 4, name: 'stray1'}); - const strayTheme2 = Theme.create({id: 5, name: 'stray2'}); + const defaultTheme = Theme.create({ id: 2, default: true, name: "default" }); + const userTheme = Theme.create({ + id: 3, + user_selectable: true, + name: "name" + }); + const strayTheme1 = Theme.create({ id: 4, name: "stray1" }); + const strayTheme2 = Theme.create({ id: 5, name: "stray2" }); const controller = this.subject({ - model: - { - content: [strayTheme2, strayTheme1, userTheme, defaultTheme] - } + model: { + content: [strayTheme2, strayTheme1, userTheme, defaultTheme] + } }); - - assert.deepEqual(controller.get('sortedThemes').map(t=>t.get('name')), [ - defaultTheme, - userTheme, - strayTheme1, - strayTheme2 - ].map(t=>t.get('name')), "sorts themes correctly"); + assert.deepEqual( + controller.get("sortedThemes").map(t => t.get("name")), + [defaultTheme, userTheme, strayTheme1, strayTheme2].map(t => t.get("name")), + "sorts themes correctly" + ); }); diff --git a/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 b/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 index 763644eadd..79a7ca7fcd 100644 --- a/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 +++ b/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 @@ -1,19 +1,44 @@ -import Badge from 'discourse/models/badge'; -import { mapRoutes } from 'discourse/mapping-router'; +import Badge from "discourse/models/badge"; +import { mapRoutes } from "discourse/mapping-router"; -moduleFor('controller:admin-user-badges', { +moduleFor("controller:admin-user-badges", { beforeEach() { - this.registry.register('router:main', mapRoutes()); + this.registry.register("router:main", mapRoutes()); }, - needs: ['controller:adminUser'] + needs: ["controller:adminUser"] }); QUnit.test("grantableBadges", function(assert) { - const badgeFirst = Badge.create({ id: 3, name: "A Badge", enabled: true, manually_grantable: true }); - const badgeMiddle = Badge.create({ id: 1, name: "My Badge", enabled: true, manually_grantable: true }); - const badgeLast = Badge.create({ id: 2, name: "Zoo Badge", enabled: true, manually_grantable: true }); - const badgeDisabled = Badge.create({ id: 4, name: "Disabled Badge", enabled: false, manually_grantable: true }); - const badgeAutomatic = Badge.create({ id: 5, name: "Automatic Badge", enabled: true, manually_grantable: false }); + const badgeFirst = Badge.create({ + id: 3, + name: "A Badge", + enabled: true, + manually_grantable: true + }); + const badgeMiddle = Badge.create({ + id: 1, + name: "My Badge", + enabled: true, + manually_grantable: true + }); + const badgeLast = Badge.create({ + id: 2, + name: "Zoo Badge", + enabled: true, + manually_grantable: true + }); + const badgeDisabled = Badge.create({ + id: 4, + name: "Disabled Badge", + enabled: false, + manually_grantable: true + }); + const badgeAutomatic = Badge.create({ + id: 5, + name: "Automatic Badge", + enabled: true, + manually_grantable: false + }); const controller = this.subject({ model: [], @@ -21,11 +46,10 @@ QUnit.test("grantableBadges", function(assert) { }); const sortedNames = [badgeFirst.name, badgeMiddle.name, badgeLast.name]; - const badgeNames = controller.get('grantableBadges').map(function(badge) { + const badgeNames = controller.get("grantableBadges").map(function(badge) { return badge.name; }); - assert.not(badgeNames.includes(badgeDisabled), "excludes disabled badges"); assert.deepEqual(badgeNames, sortedNames, "sorts badges by name"); }); diff --git a/test/javascripts/admin/models/admin-user-test.js.es6 b/test/javascripts/admin/models/admin-user-test.js.es6 index e15a2b904f..729cd55d05 100644 --- a/test/javascripts/admin/models/admin-user-test.js.es6 +++ b/test/javascripts/admin/models/admin-user-test.js.es6 @@ -1,26 +1,30 @@ -import AdminUser from 'admin/models/admin-user'; -import ApiKey from 'admin/models/api-key'; +import AdminUser from "admin/models/admin-user"; +import ApiKey from "admin/models/api-key"; QUnit.module("model:admin-user"); -QUnit.test('generate key', function(assert) { +QUnit.test("generate key", function(assert) { assert.expect(2); - var adminUser = AdminUser.create({id: 333}); - assert.ok(!adminUser.get('api_key'), 'it has no api key by default'); + var adminUser = AdminUser.create({ id: 333 }); + assert.ok(!adminUser.get("api_key"), "it has no api key by default"); return adminUser.generateApiKey().then(function() { - assert.present(adminUser.get('api_key'), 'it has an api_key now'); + assert.present(adminUser.get("api_key"), "it has an api_key now"); }); }); -QUnit.test('revoke key', function(assert) { +QUnit.test("revoke key", function(assert) { assert.expect(2); - var apiKey = ApiKey.create({id: 1234, key: 'asdfasdf'}), - adminUser = AdminUser.create({id: 333, api_key: apiKey}); + var apiKey = ApiKey.create({ id: 1234, key: "asdfasdf" }), + adminUser = AdminUser.create({ id: 333, api_key: apiKey }); - assert.equal(adminUser.get('api_key'), apiKey, 'it has the api key in the beginning'); + assert.equal( + adminUser.get("api_key"), + apiKey, + "it has the api key in the beginning" + ); return adminUser.revokeApiKey().then(function() { - assert.blank(adminUser.get('api_key'), 'it cleared the api_key'); + assert.blank(adminUser.get("api_key"), "it cleared the api_key"); }); }); diff --git a/test/javascripts/admin/models/theme-test.js.es6 b/test/javascripts/admin/models/theme-test.js.es6 index 4a16c584c8..c2e49ce3c3 100644 --- a/test/javascripts/admin/models/theme-test.js.es6 +++ b/test/javascripts/admin/models/theme-test.js.es6 @@ -1,17 +1,21 @@ -import Theme from 'admin/models/theme'; +import Theme from "admin/models/theme"; QUnit.module("model:theme"); -QUnit.test('can add an upload correctly', function(assert) { +QUnit.test("can add an upload correctly", function(assert) { let theme = Theme.create(); - assert.equal(theme.get("uploads.length"), 0, "uploads should be an empty array"); + assert.equal( + theme.get("uploads.length"), + 0, + "uploads should be an empty array" + ); - theme.setField('common', 'bob', '', 999, 2); + theme.setField("common", "bob", "", 999, 2); let fields = theme.get("theme_fields"); - assert.equal(fields.length, 1, 'expecting 1 theme field'); - assert.equal(fields[0].upload_id, 999, 'expecting upload id to be set'); - assert.equal(fields[0].type_id, 2, 'expecting type id to be set'); + assert.equal(fields.length, 1, "expecting 1 theme field"); + assert.equal(fields[0].upload_id, 999, "expecting upload id to be set"); + assert.equal(fields[0].type_id, 2, "expecting type id to be set"); assert.equal(theme.get("uploads.length"), 1, "expecting an upload"); -}); \ No newline at end of file +}); diff --git a/test/javascripts/components/ace-editor-test.js.es6 b/test/javascripts/components/ace-editor-test.js.es6 index e831da6eb5..7551a93da6 100644 --- a/test/javascripts/components/ace-editor-test.js.es6 +++ b/test/javascripts/components/ace-editor-test.js.es6 @@ -1,38 +1,50 @@ -import componentTest from 'helpers/component-test'; +import componentTest from "helpers/component-test"; -moduleForComponent('ace-editor', {integration: true}); +moduleForComponent("ace-editor", { integration: true }); -componentTest('css editor', { +componentTest("css editor", { template: '{{ace-editor mode="css"}}', test(assert) { assert.expect(1); - assert.ok(this.$('.ace_editor').length, 'it renders the ace editor'); + assert.ok(this.$(".ace_editor").length, "it renders the ace editor"); } }); -componentTest('html editor', { +componentTest("html editor", { template: '{{ace-editor mode="html" content="wat"}}', test(assert) { assert.expect(1); - assert.ok(this.$('.ace_editor').length, 'it renders the ace editor'); + assert.ok(this.$(".ace_editor").length, "it renders the ace editor"); } }); -componentTest('sql editor', { +componentTest("sql editor", { template: '{{ace-editor mode="sql" content="SELECT * FROM users"}}', test(assert) { assert.expect(1); - assert.ok(this.$('.ace_editor').length, 'it renders the ace editor'); + assert.ok(this.$(".ace_editor").length, "it renders the ace editor"); } }); -componentTest('disabled editor', { - template: '{{ace-editor mode="sql" content="SELECT * FROM users" disabled=true}}', +componentTest("disabled editor", { + template: + '{{ace-editor mode="sql" content="SELECT * FROM users" disabled=true}}', test(assert) { - const $ace = this.$('.ace_editor'); + const $ace = this.$(".ace_editor"); assert.expect(3); - assert.ok($ace.length, 'it renders the ace editor'); - assert.equal($ace.parent().data().editor.getReadOnly(), true, 'it sets ACE to read-only mode'); - assert.equal($ace.parent().attr('data-disabled'), "true", 'ACE wrapper has `data-disabled` attribute set to true'); + assert.ok($ace.length, "it renders the ace editor"); + assert.equal( + $ace + .parent() + .data() + .editor.getReadOnly(), + true, + "it sets ACE to read-only mode" + ); + assert.equal( + $ace.parent().attr("data-disabled"), + "true", + "ACE wrapper has `data-disabled` attribute set to true" + ); } }); diff --git a/test/javascripts/components/categories-admin-dropdown-test.js.es6 b/test/javascripts/components/categories-admin-dropdown-test.js.es6 index 124a4fa856..8825cf3373 100644 --- a/test/javascripts/components/categories-admin-dropdown-test.js.es6 +++ b/test/javascripts/components/categories-admin-dropdown-test.js.es6 @@ -1,8 +1,8 @@ -import componentTest from 'helpers/component-test'; -moduleForComponent('categories-admin-dropdown', {integration: true}); +import componentTest from "helpers/component-test"; +moduleForComponent("categories-admin-dropdown", { integration: true }); -componentTest('default', { - template: '{{categories-admin-dropdown}}', +componentTest("default", { + template: "{{categories-admin-dropdown}}", test(assert) { const subject = selectKit(); diff --git a/test/javascripts/components/category-chooser-test.js.es6 b/test/javascripts/components/category-chooser-test.js.es6 index 62df66fade..d8265f26cf 100644 --- a/test/javascripts/components/category-chooser-test.js.es6 +++ b/test/javascripts/components/category-chooser-test.js.es6 @@ -1,51 +1,87 @@ -import componentTest from 'helpers/component-test'; +import componentTest from "helpers/component-test"; -moduleForComponent('category-chooser', { +moduleForComponent("category-chooser", { integration: true, beforeEach: function() { - this.set('subject', selectKit()); + this.set("subject", selectKit()); } }); -componentTest('with value', { - template: '{{category-chooser value=2}}', +componentTest("with value", { + template: "{{category-chooser value=2}}", test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), 2); - assert.equal(this.get('subject').header().title(), 'feature'); + assert.equal( + this.get("subject") + .header() + .value(), + 2 + ); + assert.equal( + this.get("subject") + .header() + .title(), + "feature" + ); }); } }); -componentTest('with excludeCategoryId', { - template: '{{category-chooser excludeCategoryId=2}}', +componentTest("with excludeCategoryId", { + template: "{{category-chooser excludeCategoryId=2}}", test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.notOk(this.get('subject').rowByValue(2).exists())); + andThen(() => + assert.notOk( + this.get("subject") + .rowByValue(2) + .exists() + ) + ); } }); -componentTest('with scopedCategoryId', { - template: '{{category-chooser scopedCategoryId=2}}', +componentTest("with scopedCategoryId", { + template: "{{category-chooser scopedCategoryId=2}}", test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').rowByIndex(0).title(), 'Discussion about features or potential features of Discourse: how they work, why they work, etc.'); - assert.equal(this.get('subject').rowByIndex(0).value(), 2); - assert.equal(this.get('subject').rowByIndex(1).title(), 'My idea here is to have mini specs for features we would like built but have no bandwidth to build'); - assert.equal(this.get('subject').rowByIndex(1).value(), 26); - assert.equal(this.get('subject').rows().length, 2); + assert.equal( + this.get("subject") + .rowByIndex(0) + .title(), + "Discussion about features or potential features of Discourse: how they work, why they work, etc." + ); + assert.equal( + this.get("subject") + .rowByIndex(0) + .value(), + 2 + ); + assert.equal( + this.get("subject") + .rowByIndex(1) + .title(), + "My idea here is to have mini specs for features we would like built but have no bandwidth to build" + ); + assert.equal( + this.get("subject") + .rowByIndex(1) + .value(), + 26 + ); + assert.equal(this.get("subject").rows().length, 2); }); } }); -componentTest('with allowUncategorized=null', { - template: '{{category-chooser allowUncategorized=null}}', +componentTest("with allowUncategorized=null", { + template: "{{category-chooser allowUncategorized=null}}", beforeEach() { this.siteSettings.allow_uncategorized_topics = false; @@ -53,14 +89,24 @@ componentTest('with allowUncategorized=null', { test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), null); - assert.equal(this.get('subject').header().title(), "Select a category"); + assert.equal( + this.get("subject") + .header() + .value(), + null + ); + assert.equal( + this.get("subject") + .header() + .title(), + "Select a category" + ); }); } }); -componentTest('with allowUncategorized=null rootNone=true', { - template: '{{category-chooser allowUncategorized=null rootNone=true}}', +componentTest("with allowUncategorized=null rootNone=true", { + template: "{{category-chooser allowUncategorized=null rootNone=true}}", beforeEach() { this.siteSettings.allow_uncategorized_topics = false; @@ -68,30 +114,51 @@ componentTest('with allowUncategorized=null rootNone=true', { test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), null); - assert.equal(this.get('subject').header().title(), 'Select a category'); + assert.equal( + this.get("subject") + .header() + .value(), + null + ); + assert.equal( + this.get("subject") + .header() + .title(), + "Select a category" + ); }); } }); -componentTest('with disallowed uncategorized, rootNone and rootNoneLabel', { - template: '{{category-chooser allowUncategorized=null rootNone=true rootNoneLabel="test.root"}}', +componentTest("with disallowed uncategorized, rootNone and rootNoneLabel", { + template: + '{{category-chooser allowUncategorized=null rootNone=true rootNoneLabel="test.root"}}', beforeEach() { - I18n.translations[I18n.locale].js.test = { root: 'root none label' }; + I18n.translations[I18n.locale].js.test = { root: "root none label" }; this.siteSettings.allow_uncategorized_topics = false; }, test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), null); - assert.equal(this.get('subject').header().title(), 'Select a category'); + assert.equal( + this.get("subject") + .header() + .value(), + null + ); + assert.equal( + this.get("subject") + .header() + .title(), + "Select a category" + ); }); } }); -componentTest('with allowed uncategorized', { - template: '{{category-chooser allowUncategorized=true}}', +componentTest("with allowed uncategorized", { + template: "{{category-chooser allowUncategorized=true}}", beforeEach() { this.siteSettings.allow_uncategorized_topics = true; @@ -99,14 +166,24 @@ componentTest('with allowed uncategorized', { test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), null); - assert.equal(this.get('subject').header().title(), 'uncategorized'); + assert.equal( + this.get("subject") + .header() + .value(), + null + ); + assert.equal( + this.get("subject") + .header() + .title(), + "uncategorized" + ); }); } }); -componentTest('with allowed uncategorized and rootNone', { - template: '{{category-chooser allowUncategorized=true rootNone=true}}', +componentTest("with allowed uncategorized and rootNone", { + template: "{{category-chooser allowUncategorized=true rootNone=true}}", beforeEach() { this.siteSettings.allow_uncategorized_topics = true; @@ -114,24 +191,45 @@ componentTest('with allowed uncategorized and rootNone', { test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), null); - assert.equal(this.get('subject').header().title(), '(no category)'); + assert.equal( + this.get("subject") + .header() + .value(), + null + ); + assert.equal( + this.get("subject") + .header() + .title(), + "(no category)" + ); }); } }); -componentTest('with allowed uncategorized rootNone and rootNoneLabel', { - template: '{{category-chooser allowUncategorized=true rootNone=true rootNoneLabel="test.root"}}', +componentTest("with allowed uncategorized rootNone and rootNoneLabel", { + template: + '{{category-chooser allowUncategorized=true rootNone=true rootNoneLabel="test.root"}}', beforeEach() { - I18n.translations[I18n.locale].js.test = { root: 'root none label' }; + I18n.translations[I18n.locale].js.test = { root: "root none label" }; this.siteSettings.allow_uncategorized_topics = true; }, test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), null); - assert.equal(this.get('subject').header().title(), 'root none label'); + assert.equal( + this.get("subject") + .header() + .value(), + null + ); + assert.equal( + this.get("subject") + .header() + .title(), + "root none label" + ); }); } }); diff --git a/test/javascripts/components/category-selector-test.js.es6 b/test/javascripts/components/category-selector-test.js.es6 index befc4a9e16..d4f158d1b0 100644 --- a/test/javascripts/components/category-selector-test.js.es6 +++ b/test/javascripts/components/category-selector-test.js.es6 @@ -1,88 +1,106 @@ -import componentTest from 'helpers/component-test'; +import componentTest from "helpers/component-test"; import Category from "discourse/models/category"; -moduleForComponent('category-selector', { +moduleForComponent("category-selector", { integration: true, beforeEach: function() { - this.set('subject', selectKit()); + this.set("subject", selectKit()); } }); -componentTest('default', { - template: '{{category-selector categories=categories}}', +componentTest("default", { + template: "{{category-selector categories=categories}}", beforeEach() { - this.set('categories', [ Category.findById(2) ]); + this.set("categories", [Category.findById(2)]); }, test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), 2); + assert.equal( + this.get("subject") + .header() + .value(), + 2 + ); assert.notOk( - this.get('subject').rowByValue(2).exists(), + this.get("subject") + .rowByValue(2) + .exists(), "selected categories are not in the list" ); }); } }); -componentTest('with blacklist', { - template: '{{category-selector categories=categories blacklist=blacklist}}', +componentTest("with blacklist", { + template: "{{category-selector categories=categories blacklist=blacklist}}", beforeEach() { - this.set('categories', [ Category.findById(2) ]); - this.set('blacklist', [ Category.findById(8) ]); + this.set("categories", [Category.findById(2)]); + this.set("blacklist", [Category.findById(8)]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { assert.ok( - this.get('subject').rowByValue(6).exists(), + this.get("subject") + .rowByValue(6) + .exists(), "not blacklisted categories are in the list" ); assert.notOk( - this.get('subject').rowByValue(8).exists(), + this.get("subject") + .rowByValue(8) + .exists(), "blacklisted categories are not in the list" ); }); } }); -componentTest('interactions', { - template: '{{category-selector categories=categories}}', +componentTest("interactions", { + template: "{{category-selector categories=categories}}", beforeEach() { - this.set('categories', [ - Category.findById(2), - Category.findById(6) - ]); + this.set("categories", [Category.findById(2), Category.findById(6)]); }, test(assert) { - this.get('subject').expand().selectRowByValue(8); + this.get("subject") + .expand() + .selectRowByValue(8); andThen(() => { assert.equal( - this.get('subject').header().value(), - '2,6,8', - 'it adds the selected category' + this.get("subject") + .header() + .value(), + "2,6,8", + "it adds the selected category" ); - assert.equal(this.get('categories').length, 3); + assert.equal(this.get("categories").length, 3); }); - this.get('subject').expand(); - this.get('subject').keyboard().backspace(); - this.get('subject').keyboard().backspace(); + this.get("subject").expand(); + this.get("subject") + .keyboard() + .backspace(); + this.get("subject") + .keyboard() + .backspace(); andThen(() => { assert.equal( - this.get('subject').header().value(), - '2,6', - 'it removes the last selected category' + this.get("subject") + .header() + .value(), + "2,6", + "it removes the last selected category" ); - assert.equal(this.get('categories').length, 2); + assert.equal(this.get("categories").length, 2); }); } }); diff --git a/test/javascripts/components/combo-box-test.js.es6 b/test/javascripts/components/combo-box-test.js.es6 index 45af48de9a..b2b2dc9ffe 100644 --- a/test/javascripts/components/combo-box-test.js.es6 +++ b/test/javascripts/components/combo-box-test.js.es6 @@ -1,211 +1,328 @@ -import componentTest from 'helpers/component-test'; -moduleForComponent('combo-box', { +import componentTest from "helpers/component-test"; +moduleForComponent("combo-box", { integration: true, beforeEach: function() { - this.set('subject', selectKit()); + this.set("subject", selectKit()); } }); -componentTest('default', { - template: '{{combo-box content=items}}', +componentTest("default", { + template: "{{combo-box content=items}}", beforeEach() { - this.set('items', [{id: 1, name: 'hello'}, {id: 2, name: 'world'}]); + this.set("items", [{ id: 1, name: "hello" }, { id: 2, name: "world" }]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), "hello"); - assert.equal(this.get('subject').rowByValue(1).name(), "hello"); - assert.equal(this.get('subject').rowByValue(2).name(), "world"); + assert.equal( + this.get("subject") + .header() + .name(), + "hello" + ); + assert.equal( + this.get("subject") + .rowByValue(1) + .name(), + "hello" + ); + assert.equal( + this.get("subject") + .rowByValue(2) + .name(), + "world" + ); }); } }); -componentTest('with valueAttribute', { +componentTest("with valueAttribute", { template: '{{combo-box content=items valueAttribute="value"}}', beforeEach() { - this.set('items', [{value: 0, name: 'hello'}, {value: 1, name: 'world'}]); + this.set("items", [ + { value: 0, name: "hello" }, + { value: 1, name: "world" } + ]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').rowByValue(0).name(), "hello"); - assert.equal(this.get('subject').rowByValue(1).name(), "world"); + assert.equal( + this.get("subject") + .rowByValue(0) + .name(), + "hello" + ); + assert.equal( + this.get("subject") + .rowByValue(1) + .name(), + "world" + ); }); } }); -componentTest('with nameProperty', { +componentTest("with nameProperty", { template: '{{combo-box content=items nameProperty="text"}}', beforeEach() { - this.set('items', [{id: 0, text: 'hello'}, {id: 1, text: 'world'}]); + this.set("items", [{ id: 0, text: "hello" }, { id: 1, text: "world" }]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').rowByValue(0).name(), "hello"); - assert.equal(this.get('subject').rowByValue(1).name(), "world"); + assert.equal( + this.get("subject") + .rowByValue(0) + .name(), + "hello" + ); + assert.equal( + this.get("subject") + .rowByValue(1) + .name(), + "world" + ); }); } }); -componentTest('with an array as content', { - template: '{{combo-box content=items value=value}}', +componentTest("with an array as content", { + template: "{{combo-box content=items value=value}}", beforeEach() { - this.set('items', ['evil', 'trout', 'hat']); + this.set("items", ["evil", "trout", "hat"]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').rowByValue('evil').name(), "evil"); - assert.equal(this.get('subject').rowByValue('trout').name(), "trout"); + assert.equal( + this.get("subject") + .rowByValue("evil") + .name(), + "evil" + ); + assert.equal( + this.get("subject") + .rowByValue("trout") + .name(), + "trout" + ); }); } }); -componentTest('with value and none as a string', { +componentTest("with value and none as a string", { template: '{{combo-box content=items none="test.none" value=value}}', beforeEach() { - I18n.translations[I18n.locale].js.test = {none: 'none'}; - this.set('items', ['evil', 'trout', 'hat']); - this.set('value', 'trout'); + I18n.translations[I18n.locale].js.test = { none: "none" }; + this.set("items", ["evil", "trout", "hat"]); + this.set("value", "trout"); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').noneRow().name(), 'none'); - assert.equal(this.get('subject').rowByValue("evil").name(), "evil"); - assert.equal(this.get('subject').rowByValue("trout").name(), "trout"); - assert.equal(this.get('subject').header().name(), 'trout'); - assert.equal(this.get('value'), 'trout'); + assert.equal( + this.get("subject") + .noneRow() + .name(), + "none" + ); + assert.equal( + this.get("subject") + .rowByValue("evil") + .name(), + "evil" + ); + assert.equal( + this.get("subject") + .rowByValue("trout") + .name(), + "trout" + ); + assert.equal( + this.get("subject") + .header() + .name(), + "trout" + ); + assert.equal(this.get("value"), "trout"); }); - this.get('subject').selectNoneRow(); + this.get("subject").selectNoneRow(); andThen(() => { - assert.equal(this.get('value'), null); + assert.equal(this.get("value"), null); }); } }); -componentTest('with value and none as an object', { - template: '{{combo-box content=items none=none value=value}}', +componentTest("with value and none as an object", { + template: "{{combo-box content=items none=none value=value}}", beforeEach() { - this.set('none', { id: 'something', name: 'none' }); - this.set('items', ['evil', 'trout', 'hat']); - this.set('value', 'evil'); + this.set("none", { id: "something", name: "none" }); + this.set("items", ["evil", "trout", "hat"]); + this.set("value", "evil"); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').noneRow().name(), 'none'); - assert.equal(this.get('subject').rowByValue("evil").name(), "evil"); - assert.equal(this.get('subject').rowByValue("trout").name(), "trout"); - assert.equal(this.get('subject').header().name(), 'evil'); - assert.equal(this.get('value'), 'evil'); + assert.equal( + this.get("subject") + .noneRow() + .name(), + "none" + ); + assert.equal( + this.get("subject") + .rowByValue("evil") + .name(), + "evil" + ); + assert.equal( + this.get("subject") + .rowByValue("trout") + .name(), + "trout" + ); + assert.equal( + this.get("subject") + .header() + .name(), + "evil" + ); + assert.equal(this.get("value"), "evil"); }); - this.get('subject').selectNoneRow(); + this.get("subject").selectNoneRow(); andThen(() => { - assert.equal(this.get('value'), null); + assert.equal(this.get("value"), null); }); } }); -componentTest('with no value and none as an object', { - template: '{{combo-box content=items none=none value=value}}', +componentTest("with no value and none as an object", { + template: "{{combo-box content=items none=none value=value}}", beforeEach() { - I18n.translations[I18n.locale].js.test = {none: 'none'}; - this.set('none', { id: 'something', name: 'none' }); - this.set('items', ['evil', 'trout', 'hat']); - this.set('value', null); + I18n.translations[I18n.locale].js.test = { none: "none" }; + this.set("none", { id: "something", name: "none" }); + this.set("items", ["evil", "trout", "hat"]); + this.set("value", null); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), 'none'); + assert.equal( + this.get("subject") + .header() + .name(), + "none" + ); }); } }); -componentTest('with no value and none string', { - template: '{{combo-box content=items none=none value=value}}', +componentTest("with no value and none string", { + template: "{{combo-box content=items none=none value=value}}", beforeEach() { - I18n.translations[I18n.locale].js.test = {none: 'none'}; - this.set('none', 'test.none'); - this.set('items', ['evil', 'trout', 'hat']); - this.set('value', null); + I18n.translations[I18n.locale].js.test = { none: "none" }; + this.set("none", "test.none"); + this.set("items", ["evil", "trout", "hat"]); + this.set("value", null); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), 'none'); + assert.equal( + this.get("subject") + .header() + .name(), + "none" + ); }); } }); -componentTest('with no value and no none', { - template: '{{combo-box content=items value=value}}', +componentTest("with no value and no none", { + template: "{{combo-box content=items value=value}}", beforeEach() { - this.set('items', ['evil', 'trout', 'hat']); - this.set('value', null); + this.set("items", ["evil", "trout", "hat"]); + this.set("value", null); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), 'evil', 'it sets the first row as value'); + assert.equal( + this.get("subject") + .header() + .name(), + "evil", + "it sets the first row as value" + ); }); } }); -componentTest('with empty string as value', { - template: '{{combo-box content=items value=value}}', +componentTest("with empty string as value", { + template: "{{combo-box content=items value=value}}", beforeEach() { - this.set('items', ['evil', 'trout', 'hat']); - this.set('value', ''); + this.set("items", ["evil", "trout", "hat"]); + this.set("value", ""); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), 'evil', 'it sets the first row as value'); + assert.equal( + this.get("subject") + .header() + .name(), + "evil", + "it sets the first row as value" + ); }); } }); -componentTest('with noneLabel', { - template: '{{combo-box content=items allowAutoSelectFirst=false noneLabel=noneLabel}}', +componentTest("with noneLabel", { + template: + "{{combo-box content=items allowAutoSelectFirst=false noneLabel=noneLabel}}", beforeEach() { - I18n.translations[I18n.locale].js.test = {none: 'none'}; - this.set('items', ['evil', 'trout', 'hat']); - this.set('noneLabel', 'test.none'); + I18n.translations[I18n.locale].js.test = { none: "none" }; + this.set("items", ["evil", "trout", "hat"]); + this.set("noneLabel", "test.none"); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), 'none', 'it displays noneLabel as the header name'); + assert.equal( + this.get("subject") + .header() + .name(), + "none", + "it displays noneLabel as the header name" + ); }); } }); diff --git a/test/javascripts/components/d-button-test.js.es6 b/test/javascripts/components/d-button-test.js.es6 index d283da4f21..f07d6926c2 100644 --- a/test/javascripts/components/d-button-test.js.es6 +++ b/test/javascripts/components/d-button-test.js.es6 @@ -1,31 +1,37 @@ -import componentTest from 'helpers/component-test'; -moduleForComponent('d-button', {integration: true}); +import componentTest from "helpers/component-test"; +moduleForComponent("d-button", { integration: true }); -componentTest('icon only button', { +componentTest("icon only button", { template: '{{d-button icon="plus" tabindex="3"}}', test(assert) { - assert.ok(this.$('button.btn.btn-icon.no-text').length, 'it has all the classes'); - assert.ok(this.$('button .d-icon.d-icon-plus').length, 'it has the icon'); - assert.equal(this.$('button').attr('tabindex'), "3", 'it has the tabindex'); + assert.ok( + this.$("button.btn.btn-icon.no-text").length, + "it has all the classes" + ); + assert.ok(this.$("button .d-icon.d-icon-plus").length, "it has the icon"); + assert.equal(this.$("button").attr("tabindex"), "3", "it has the tabindex"); } }); -componentTest('icon and text button', { +componentTest("icon and text button", { template: '{{d-button icon="plus" label="topic.create"}}', test(assert) { - assert.ok(this.$('button.btn.btn-icon-text').length, 'it has all the classes'); - assert.ok(this.$('button .d-icon.d-icon-plus').length, 'it has the icon'); - assert.ok(this.$('button span.d-button-label').length, 'it has the label'); + assert.ok( + this.$("button.btn.btn-icon-text").length, + "it has all the classes" + ); + assert.ok(this.$("button .d-icon.d-icon-plus").length, "it has the icon"); + assert.ok(this.$("button span.d-button-label").length, "it has the label"); } }); -componentTest('text only button', { +componentTest("text only button", { template: '{{d-button label="topic.create"}}', test(assert) { - assert.ok(this.$('button.btn.btn-text').length, 'it has all the classes'); - assert.ok(this.$('button span.d-button-label').length, 'it has the label'); + assert.ok(this.$("button.btn.btn-text").length, "it has all the classes"); + assert.ok(this.$("button span.d-button-label").length, "it has the label"); } }); diff --git a/test/javascripts/components/d-editor-test.js.es6 b/test/javascripts/components/d-editor-test.js.es6 index e0409e4957..4bf18bca96 100644 --- a/test/javascripts/components/d-editor-test.js.es6 +++ b/test/javascripts/components/d-editor-test.js.es6 @@ -1,45 +1,67 @@ -import componentTest from 'helpers/component-test'; -import { withPluginApi } from 'discourse/lib/plugin-api'; +import componentTest from "helpers/component-test"; +import { withPluginApi } from "discourse/lib/plugin-api"; -moduleForComponent('d-editor', {integration: true}); +moduleForComponent("d-editor", { integration: true }); -componentTest('preview updates with markdown', { - template: '{{d-editor value=value}}', +componentTest("preview updates with markdown", { + template: "{{d-editor value=value}}", test(assert) { - assert.ok(this.$('.d-editor-button-bar').length); - fillIn('.d-editor-input', 'hello **world**'); + assert.ok(this.$(".d-editor-button-bar").length); + fillIn(".d-editor-input", "hello **world**"); andThen(() => { - assert.equal(this.get('value'), 'hello **world**'); - assert.equal(this.$('.d-editor-preview').html().trim(), '

hello world

'); + assert.equal(this.get("value"), "hello **world**"); + assert.equal( + this.$(".d-editor-preview") + .html() + .trim(), + "

hello world

" + ); }); } }); -componentTest('preview sanitizes HTML', { - template: '{{d-editor value=value}}', +componentTest("preview sanitizes HTML", { + template: "{{d-editor value=value}}", test(assert) { - fillIn('.d-editor-input', `">`); + fillIn(".d-editor-input", `">`); andThen(() => { - assert.equal(this.$('.d-editor-preview').html().trim(), '

\">

'); + assert.equal( + this.$(".d-editor-preview") + .html() + .trim(), + '

">

' + ); }); } }); -componentTest('updating the value refreshes the preview', { - template: '{{d-editor value=value}}', +componentTest("updating the value refreshes the preview", { + template: "{{d-editor value=value}}", beforeEach() { - this.set('value', 'evil trout'); + this.set("value", "evil trout"); }, test(assert) { - assert.equal(this.$('.d-editor-preview').html().trim(), '

evil trout

'); + assert.equal( + this.$(".d-editor-preview") + .html() + .trim(), + "

evil trout

" + ); - andThen(() => this.set('value', 'zogstrip')); - andThen(() => assert.equal(this.$('.d-editor-preview').html().trim(), '

zogstrip

')); + andThen(() => this.set("value", "zogstrip")); + andThen(() => + assert.equal( + this.$(".d-editor-preview") + .html() + .trim(), + "

zogstrip

" + ) + ); } }); @@ -51,12 +73,12 @@ function jumpEnd(textarea) { function testCase(title, testFunc) { componentTest(title, { - template: '{{d-editor value=value}}', + template: "{{d-editor value=value}}", beforeEach() { - this.set('value', 'hello world.'); + this.set("value", "hello world."); }, test(assert) { - const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); + const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); testFunc.call(this, assert, textarea); } }); @@ -64,12 +86,12 @@ function testCase(title, testFunc) { function composerTestCase(title, testFunc) { componentTest(title, { - template: '{{d-editor value=value composerEvents=true}}', + template: "{{d-editor value=value composerEvents=true}}", beforeEach() { - this.set('value', 'hello world.'); + this.set("value", "hello world."); }, test(assert) { - const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); + const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); testFunc.call(this, assert, textarea); } }); @@ -81,7 +103,7 @@ testCase(`selecting the space before a word`, function(assert, textarea) { click(`button.bold`); andThen(() => { - assert.equal(this.get('value'), `hello **w**orld.`); + assert.equal(this.get("value"), `hello **w**orld.`); assert.equal(textarea.selectionStart, 8); assert.equal(textarea.selectionEnd, 9); }); @@ -93,7 +115,7 @@ testCase(`selecting the space after a word`, function(assert, textarea) { click(`button.bold`); andThen(() => { - assert.equal(this.get('value'), `**hello** world.`); + assert.equal(this.get("value"), `**hello** world.`); assert.equal(textarea.selectionStart, 2); assert.equal(textarea.selectionEnd, 7); }); @@ -103,7 +125,7 @@ testCase(`bold button with no selection`, function(assert, textarea) { click(`button.bold`); andThen(() => { const example = I18n.t(`composer.bold_text`); - assert.equal(this.get('value'), `hello world.**${example}**`); + assert.equal(this.get("value"), `hello world.**${example}**`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 14 + example.length); }); @@ -115,21 +137,21 @@ testCase(`bold button with a selection`, function(assert, textarea) { click(`button.bold`); andThen(() => { - assert.equal(this.get('value'), `hello **world**.`); + assert.equal(this.get("value"), `hello **world**.`); assert.equal(textarea.selectionStart, 8); assert.equal(textarea.selectionEnd, 13); }); click(`button.bold`); andThen(() => { - assert.equal(this.get('value'), 'hello world.'); + assert.equal(this.get("value"), "hello world."); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 11); }); }); -testCase(`bold with a multiline selection`, function (assert, textarea) { - this.set('value', "hello\n\nworld\n\ntest."); +testCase(`bold with a multiline selection`, function(assert, textarea) { + this.set("value", "hello\n\nworld\n\ntest."); andThen(() => { textarea.selectionStart = 0; @@ -138,14 +160,14 @@ testCase(`bold with a multiline selection`, function (assert, textarea) { click(`button.bold`); andThen(() => { - assert.equal(this.get('value'), `**hello**\n\n**world**\n\ntest.`); + assert.equal(this.get("value"), `**hello**\n\n**world**\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 20); }); click(`button.bold`); andThen(() => { - assert.equal(this.get('value'), `hello\n\nworld\n\ntest.`); + assert.equal(this.get("value"), `hello\n\nworld\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 12); }); @@ -155,7 +177,7 @@ testCase(`italic button with no selection`, function(assert, textarea) { click(`button.italic`); andThen(() => { const example = I18n.t(`composer.italic_text`); - assert.equal(this.get('value'), `hello world._${example}_`); + assert.equal(this.get("value"), `hello world._${example}_`); assert.equal(textarea.selectionStart, 13); assert.equal(textarea.selectionEnd, 13 + example.length); @@ -168,21 +190,21 @@ testCase(`italic button with a selection`, function(assert, textarea) { click(`button.italic`); andThen(() => { - assert.equal(this.get('value'), `hello _world_.`); + assert.equal(this.get("value"), `hello _world_.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 12); }); click(`button.italic`); andThen(() => { - assert.equal(this.get('value'), 'hello world.'); + assert.equal(this.get("value"), "hello world."); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 11); }); }); -testCase(`italic with a multiline selection`, function (assert, textarea) { - this.set('value', "hello\n\nworld\n\ntest."); +testCase(`italic with a multiline selection`, function(assert, textarea) { + this.set("value", "hello\n\nworld\n\ntest."); andThen(() => { textarea.selectionStart = 0; @@ -191,108 +213,117 @@ testCase(`italic with a multiline selection`, function (assert, textarea) { click(`button.italic`); andThen(() => { - assert.equal(this.get('value'), `_hello_\n\n_world_\n\ntest.`); + assert.equal(this.get("value"), `_hello_\n\n_world_\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 16); }); click(`button.italic`); andThen(() => { - assert.equal(this.get('value'), `hello\n\nworld\n\ntest.`); + assert.equal(this.get("value"), `hello\n\nworld\n\ntest.`); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 12); }); }); -testCase('link modal (cancel)', function(assert) { - assert.equal(this.$('.insert-link.hidden').length, 1); +testCase("link modal (cancel)", function(assert) { + assert.equal(this.$(".insert-link.hidden").length, 1); - click('button.link'); + click("button.link"); andThen(() => { - assert.equal(this.$('.insert-link.hidden').length, 0); + assert.equal(this.$(".insert-link.hidden").length, 0); }); - click('.insert-link button.btn-danger'); + click(".insert-link button.btn-danger"); andThen(() => { - assert.equal(this.$('.insert-link.hidden').length, 1); - assert.equal(this.get('value'), 'hello world.'); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal(this.get("value"), "hello world."); }); }); -testCase('link modal (simple link)', function(assert, textarea) { - click('button.link'); +testCase("link modal (simple link)", function(assert, textarea) { + click("button.link"); - const url = 'http://eviltrout.com'; + const url = "http://eviltrout.com"; - fillIn('.insert-link input.link-url', url); - click('.insert-link button.btn-primary'); + fillIn(".insert-link input.link-url", url); + click(".insert-link button.btn-primary"); andThen(() => { - assert.equal(this.$('.insert-link.hidden').length, 1); - assert.equal(this.get('value'), `hello world.[${url}](${url})`); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal(this.get("value"), `hello world.[${url}](${url})`); assert.equal(textarea.selectionStart, 13); assert.equal(textarea.selectionEnd, 13 + url.length); }); }); -testCase('link modal auto http addition', function(assert) { - click('button.link'); - fillIn('.insert-link input.link-url', 'sam.com'); - click('.insert-link button.btn-primary'); +testCase("link modal auto http addition", function(assert) { + click("button.link"); + fillIn(".insert-link input.link-url", "sam.com"); + click(".insert-link button.btn-primary"); andThen(() => { - assert.equal(this.get('value'), `hello world.[sam.com](http://sam.com)`); + assert.equal(this.get("value"), `hello world.[sam.com](http://sam.com)`); }); }); -testCase('link modal (simple link) with selected text', function(assert, textarea) { +testCase("link modal (simple link) with selected text", function( + assert, + textarea +) { textarea.selectionStart = 0; textarea.selectionEnd = 12; - click('button.link'); + click("button.link"); andThen(() => { - assert.equal(this.$('input.link-text')[0].value, 'hello world.'); + assert.equal(this.$("input.link-text")[0].value, "hello world."); }); - fillIn('.insert-link input.link-url', 'http://eviltrout.com'); - click('.insert-link button.btn-primary'); + fillIn(".insert-link input.link-url", "http://eviltrout.com"); + click(".insert-link button.btn-primary"); andThen(() => { - assert.equal(this.$('.insert-link.hidden').length, 1); - assert.equal(this.get('value'), '[hello world.](http://eviltrout.com)'); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal(this.get("value"), "[hello world.](http://eviltrout.com)"); }); }); -testCase('link modal (link with description)', function(assert) { - click('button.link'); - fillIn('.insert-link input.link-url', 'http://eviltrout.com'); - fillIn('.insert-link input.link-text', 'evil trout'); - click('.insert-link button.btn-primary'); +testCase("link modal (link with description)", function(assert) { + click("button.link"); + fillIn(".insert-link input.link-url", "http://eviltrout.com"); + fillIn(".insert-link input.link-text", "evil trout"); + click(".insert-link button.btn-primary"); andThen(() => { - assert.equal(this.$('.insert-link.hidden').length, 1); - assert.equal(this.get('value'), 'hello world.[evil trout](http://eviltrout.com)'); + assert.equal(this.$(".insert-link.hidden").length, 1); + assert.equal( + this.get("value"), + "hello world.[evil trout](http://eviltrout.com)" + ); }); }); -componentTest('advanced code', { - template: '{{d-editor value=value}}', +componentTest("advanced code", { + template: "{{d-editor value=value}}", beforeEach() { - this.siteSettings.code_formatting_style = '4-spaces-indent'; - this.set('value', -` + this.siteSettings.code_formatting_style = "4-spaces-indent"; + this.set( + "value", + ` function xyz(x, y, z) { if (y === z) { return true; } } -` ); +` + ); }, test(assert) { - const textarea = this.$('textarea.d-editor-input')[0]; + const textarea = this.$("textarea.d-editor-input")[0]; textarea.selectionStart = 0; textarea.selectionEnd = textarea.value.length; - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -` + assert.equal( + this.get("value"), + ` function xyz(x, y, z) { if (y === z) { return true; @@ -302,54 +333,52 @@ function xyz(x, y, z) { ); }); } - }); -componentTest('code button', { - template: '{{d-editor value=value}}', +componentTest("code button", { + template: "{{d-editor value=value}}", beforeEach() { - this.siteSettings.code_formatting_style = '4-spaces-indent'; + this.siteSettings.code_formatting_style = "4-spaces-indent"; }, test(assert) { - const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); + const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -` ${I18n.t('composer.code_text')}` - ); + assert.equal(this.get("value"), ` ${I18n.t("composer.code_text")}`); - this.set('value', "first line\n\nsecond line\n\nthird line"); + this.set("value", "first line\n\nsecond line\n\nthird line"); textarea.selectionStart = 11; textarea.selectionEnd = 11; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`first line - ${I18n.t('composer.code_text')} + assert.equal( + this.get("value"), + `first line + ${I18n.t("composer.code_text")} second line third line` ); - this.set('value', "first line\n\nsecond line\n\nthird line"); + this.set("value", "first line\n\nsecond line\n\nthird line"); }); - - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`first line + assert.equal( + this.get("value"), + `first line second line -third line\`${I18n.t('composer.code_title')}\`` +third line\`${I18n.t("composer.code_title")}\`` ); - this.set('value', "first line\n\nsecond line\n\nthird line"); + this.set("value", "first line\n\nsecond line\n\nthird line"); }); andThen(() => { @@ -357,16 +386,17 @@ third line\`${I18n.t('composer.code_title')}\`` textarea.selectionEnd = 5; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`first\`${I18n.t('composer.code_title')}\` line + assert.equal( + this.get("value"), + `first\`${I18n.t("composer.code_title")}\` line second line third line` ); - this.set('value', "first line\n\nsecond line\n\nthird line"); + this.set("value", "first line\n\nsecond line\n\nthird line"); }); andThen(() => { @@ -374,16 +404,22 @@ third line` textarea.selectionEnd = 10; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), "first `line`\n\nsecond line\n\nthird line"); + assert.equal( + this.get("value"), + "first `line`\n\nsecond line\n\nthird line" + ); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 11); }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), "first line\n\nsecond line\n\nthird line"); + assert.equal( + this.get("value"), + "first line\n\nsecond line\n\nthird line" + ); assert.equal(textarea.selectionStart, 6); assert.equal(textarea.selectionEnd, 10); @@ -391,35 +427,42 @@ third line` textarea.selectionEnd = 23; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), " first line\n\n second line\n\nthird line"); + assert.equal( + this.get("value"), + " first line\n\n second line\n\nthird line" + ); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 31); }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), "first line\n\nsecond line\n\nthird line"); + assert.equal( + this.get("value"), + "first line\n\nsecond line\n\nthird line" + ); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 23); }); } }); -componentTest('code fences', { - template: '{{d-editor value=value}}', +componentTest("code fences", { + template: "{{d-editor value=value}}", beforeEach() { - this.set('value', ''); + this.set("value", ""); }, test(assert) { - const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); + const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`\`\`\` + assert.equal( + this.get("value"), + `\`\`\` ${I18n.t("composer.paste_code_text")} \`\`\`` ); @@ -427,16 +470,17 @@ ${I18n.t("composer.paste_code_text")} assert.equal(textarea.selectionStart, 4); assert.equal(textarea.selectionEnd, 27); - this.set('value', 'first line\nsecond line\nthird line'); + this.set("value", "first line\nsecond line\nthird line"); textarea.selectionStart = 0; textarea.selectionEnd = textarea.value.length; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`\`\`\` + assert.equal( + this.get("value"), + `\`\`\` first line second line third line @@ -447,33 +491,38 @@ third line assert.equal(textarea.selectionStart, textarea.value.length); assert.equal(textarea.selectionEnd, textarea.value.length); - this.set('value', 'first line\nsecond line\nthird line'); + this.set("value", "first line\nsecond line\nthird line"); textarea.selectionStart = 0; textarea.selectionEnd = 0; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`\`${I18n.t('composer.code_title')}\`first line + assert.equal( + this.get("value"), + `\`${I18n.t("composer.code_title")}\`first line second line third line` ); assert.equal(textarea.selectionStart, 1); - assert.equal(textarea.selectionEnd, I18n.t('composer.code_title').length + 1); + assert.equal( + textarea.selectionEnd, + I18n.t("composer.code_title").length + 1 + ); - this.set('value', 'first line\nsecond line\nthird line'); + this.set("value", "first line\nsecond line\nthird line"); textarea.selectionStart = 0; textarea.selectionEnd = 10; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`\`first line\` + assert.equal( + this.get("value"), + `\`first line\` second line third line` ); @@ -481,16 +530,17 @@ third line` assert.equal(textarea.selectionStart, 1); assert.equal(textarea.selectionEnd, 11); - this.set('value', 'first line\nsecond line\nthird line'); + this.set("value", "first line\nsecond line\nthird line"); textarea.selectionStart = 0; textarea.selectionEnd = 23; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), -`\`\`\` + assert.equal( + this.get("value"), + `\`\`\` first line second line \`\`\` @@ -500,15 +550,18 @@ third line` assert.equal(textarea.selectionStart, 30); assert.equal(textarea.selectionEnd, 30); - this.set('value', 'first line\nsecond line\nthird line'); + this.set("value", "first line\nsecond line\nthird line"); textarea.selectionStart = 6; textarea.selectionEnd = 17; }); - click('button.code'); + click("button.code"); andThen(() => { - assert.equal(this.get('value'), `first \n\`\`\`\nline\nsecond\n\`\`\`\n line\nthird line`); + assert.equal( + this.get("value"), + `first \n\`\`\`\nline\nsecond\n\`\`\`\n line\nthird line` + ); assert.equal(textarea.selectionStart, 27); assert.equal(textarea.selectionEnd, 27); @@ -516,71 +569,69 @@ third line` } }); - componentTest("quote button - empty lines", { - template: '{{d-editor value=value composerEvents=true}}', + template: "{{d-editor value=value composerEvents=true}}", beforeEach() { - this.set('value', "one\n\ntwo\n\nthree"); + this.set("value", "one\n\ntwo\n\nthree"); }, test(assert) { - const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); + const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); andThen(() => { textarea.selectionStart = 0; }); - click('button.quote'); + click("button.quote"); andThen(() => { - assert.equal(this.get('value'), "> one\n> \n> two\n> \n> three"); + assert.equal(this.get("value"), "> one\n> \n> two\n> \n> three"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 25); }); - click('button.quote'); + click("button.quote"); andThen(() => { - assert.equal(this.get('value'), "one\n\ntwo\n\nthree"); + assert.equal(this.get("value"), "one\n\ntwo\n\nthree"); }); } }); componentTest("quote button - selecting empty lines", { - template: '{{d-editor value=value composerEvents=true}}', + template: "{{d-editor value=value composerEvents=true}}", beforeEach() { - this.set('value', "one\n\n\n\ntwo"); + this.set("value", "one\n\n\n\ntwo"); }, test(assert) { - const textarea = jumpEnd(this.$('textarea.d-editor-input')[0]); + const textarea = jumpEnd(this.$("textarea.d-editor-input")[0]); andThen(() => { textarea.selectionStart = 6; textarea.selectionEnd = 10; }); - click('button.quote'); + click("button.quote"); andThen(() => { - assert.equal(this.get('value'), "one\n\n\n> \n> two"); + assert.equal(this.get("value"), "one\n\n\n> \n> two"); }); } }); -testCase('quote button', function(assert, textarea) { - +testCase("quote button", function(assert, textarea) { andThen(() => { textarea.selectionStart = 6; textarea.selectionEnd = 9; }); - click('button.quote'); + click("button.quote"); andThen(() => { - assert.equal(this.get('value'), 'hello\n\n> wor\n\nld.'); + assert.equal(this.get("value"), "hello\n\n> wor\n\nld."); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 12); }); - click('button.quote'); + click("button.quote"); andThen(() => { - assert.equal(this.get('value'), 'hello\n\nwor\n\nld.'); + assert.equal(this.get("value"), "hello\n\nwor\n\nld."); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 10); }); @@ -590,26 +641,25 @@ testCase('quote button', function(assert, textarea) { textarea.selectionEnd = 15; }); - click('button.quote'); + click("button.quote"); andThen(() => { - assert.equal(this.get('value'), 'hello\n\nwor\n\nld.\n\n> Blockquote'); + assert.equal(this.get("value"), "hello\n\nwor\n\nld.\n\n> Blockquote"); }); - }); testCase(`bullet button with no selection`, function(assert, textarea) { - const example = I18n.t('composer.list_item'); + const example = I18n.t("composer.list_item"); click(`button.bullet`); andThen(() => { - assert.equal(this.get('value'), `hello world.\n\n* ${example}`); + assert.equal(this.get("value"), `hello world.\n\n* ${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 16 + example.length); }); click(`button.bullet`); andThen(() => { - assert.equal(this.get('value'), `hello world.\n\n${example}`); + assert.equal(this.get("value"), `hello world.\n\n${example}`); }); }); @@ -619,21 +669,24 @@ testCase(`bullet button with a selection`, function(assert, textarea) { click(`button.bullet`); andThen(() => { - assert.equal(this.get('value'), `hello\n\n* world\n\n.`); + assert.equal(this.get("value"), `hello\n\n* world\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 14); }); click(`button.bullet`); andThen(() => { - assert.equal(this.get('value'), `hello\n\nworld\n\n.`); + assert.equal(this.get("value"), `hello\n\nworld\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 12); }); }); -testCase(`bullet button with a multiple line selection`, function(assert, textarea) { - this.set('value', "* Hello\n\nWorld\n\nEvil"); +testCase(`bullet button with a multiple line selection`, function( + assert, + textarea +) { + this.set("value", "* Hello\n\nWorld\n\nEvil"); andThen(() => { textarea.selectionStart = 0; @@ -642,32 +695,32 @@ testCase(`bullet button with a multiple line selection`, function(assert, textar click(`button.bullet`); andThen(() => { - assert.equal(this.get('value'), "Hello\n\nWorld\n\nEvil"); + assert.equal(this.get("value"), "Hello\n\nWorld\n\nEvil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 18); }); click(`button.bullet`); andThen(() => { - assert.equal(this.get('value'), "* Hello\n\n* World\n\n* Evil"); + assert.equal(this.get("value"), "* Hello\n\n* World\n\n* Evil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 24); }); }); testCase(`list button with no selection`, function(assert, textarea) { - const example = I18n.t('composer.list_item'); + const example = I18n.t("composer.list_item"); click(`button.list`); andThen(() => { - assert.equal(this.get('value'), `hello world.\n\n1. ${example}`); + assert.equal(this.get("value"), `hello world.\n\n1. ${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 17 + example.length); }); click(`button.list`); andThen(() => { - assert.equal(this.get('value'), `hello world.\n\n${example}`); + assert.equal(this.get("value"), `hello world.\n\n${example}`); assert.equal(textarea.selectionStart, 14); assert.equal(textarea.selectionEnd, 14 + example.length); }); @@ -679,21 +732,21 @@ testCase(`list button with a selection`, function(assert, textarea) { click(`button.list`); andThen(() => { - assert.equal(this.get('value'), `hello\n\n1. world\n\n.`); + assert.equal(this.get("value"), `hello\n\n1. world\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 15); }); click(`button.list`); andThen(() => { - assert.equal(this.get('value'), `hello\n\nworld\n\n.`); + assert.equal(this.get("value"), `hello\n\nworld\n\n.`); assert.equal(textarea.selectionStart, 7); assert.equal(textarea.selectionEnd, 12); }); }); testCase(`list button with line sequence`, function(assert, textarea) { - this.set('value', "Hello\n\nWorld\n\nEvil"); + this.set("value", "Hello\n\nWorld\n\nEvil"); andThen(() => { textarea.selectionStart = 0; @@ -702,46 +755,45 @@ testCase(`list button with line sequence`, function(assert, textarea) { click(`button.list`); andThen(() => { - assert.equal(this.get('value'), "1. Hello\n\n2. World\n\n3. Evil"); + assert.equal(this.get("value"), "1. Hello\n\n2. World\n\n3. Evil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 27); }); click(`button.list`); andThen(() => { - assert.equal(this.get('value'), "Hello\n\nWorld\n\nEvil"); + assert.equal(this.get("value"), "Hello\n\nWorld\n\nEvil"); assert.equal(textarea.selectionStart, 0); assert.equal(textarea.selectionEnd, 18); }); }); -componentTest('clicking the toggle-direction button toggles the direction', { - template: '{{d-editor value=value}}', +componentTest("clicking the toggle-direction button toggles the direction", { + template: "{{d-editor value=value}}", beforeEach() { this.siteSettings.support_mixed_text_direction = true; this.siteSettings.default_locale = "en"; }, test(assert) { - const textarea = this.$('textarea.d-editor-input'); - click('button.toggle-direction'); + const textarea = this.$("textarea.d-editor-input"); + click("button.toggle-direction"); andThen(() => { - assert.equal(textarea.attr('dir'), 'rtl'); + assert.equal(textarea.attr("dir"), "rtl"); }); - click('button.toggle-direction'); + click("button.toggle-direction"); andThen(() => { - assert.equal(textarea.attr('dir'), 'ltr'); + assert.equal(textarea.attr("dir"), "ltr"); }); } }); testCase(`doesn't jump to bottom with long text`, function(assert, textarea) { - - let longText = 'hello world.'; - for (let i=0; i<8; i++) { + let longText = "hello world."; + for (let i = 0; i < 8; i++) { longText = longText + longText; } - this.set('value', longText); + this.set("value", longText); andThen(() => { $(textarea).scrollTop(0); @@ -749,110 +801,127 @@ testCase(`doesn't jump to bottom with long text`, function(assert, textarea) { textarea.selectionEnd = 3; }); - click('button.bold'); + click("button.bold"); andThen(() => { - assert.equal($(textarea).scrollTop(), 0, 'it stays scrolled up'); + assert.equal($(textarea).scrollTop(), 0, "it stays scrolled up"); }); }); -componentTest('emoji', { - template: '{{d-editor value=value}}', +componentTest("emoji", { + template: "{{d-editor value=value}}", beforeEach() { // Test adding a custom button - withPluginApi('0.1', api => { + withPluginApi("0.1", api => { api.onToolbarCreate(toolbar => { toolbar.addButton({ - id: 'emoji', - group: 'extras', - icon: 'smile-o', - action: 'emoji' + id: "emoji", + group: "extras", + icon: "smile-o", + action: "emoji" }); }); }); - this.set('value', 'hello world.'); + this.set("value", "hello world."); }, test(assert) { - jumpEnd(this.$('textarea.d-editor-input')[0]); - click('button.emoji'); + jumpEnd(this.$("textarea.d-editor-input")[0]); + click("button.emoji"); - click('.emoji-picker .section[data-section="people"] button.emoji[title="grinning"]'); + click( + '.emoji-picker .section[data-section="people"] button.emoji[title="grinning"]' + ); andThen(() => { - assert.equal(this.get('value'), 'hello world.:grinning:'); + assert.equal(this.get("value"), "hello world.:grinning:"); }); } }); testCase("replace-text event by default", function(assert) { - this.set('value', "red green blue"); + this.set("value", "red green blue"); andThen(() => { - this.container.lookup('app-events:main').trigger('composer:replace-text', 'green', 'yellow'); + this.container + .lookup("app-events:main") + .trigger("composer:replace-text", "green", "yellow"); }); andThen(() => { - assert.equal(this.get('value'), 'red green blue'); + assert.equal(this.get("value"), "red green blue"); }); }); composerTestCase("replace-text event for composer", function(assert) { - this.set('value', "red green blue"); + this.set("value", "red green blue"); andThen(() => { - this.container.lookup('app-events:main').trigger('composer:replace-text', 'green', 'yellow'); + this.container + .lookup("app-events:main") + .trigger("composer:replace-text", "green", "yellow"); }); andThen(() => { - assert.equal(this.get('value'), 'red yellow blue'); + assert.equal(this.get("value"), "red yellow blue"); }); }); - (() => { // Tests to check cursor/selection after replace-text event. - const BEFORE = 'red green blue'; - const NEEDLE = 'green'; - const REPLACE = 'yellow'; + const BEFORE = "red green blue"; + const NEEDLE = "green"; + const REPLACE = "yellow"; const AFTER = BEFORE.replace(NEEDLE, REPLACE); const CASES = [ { - description: 'cursor at start remains there', + description: "cursor at start remains there", before: [0, 0], after: [0, 0] - },{ - description: 'cursor before needle becomes cursor before replacement', + }, + { + description: "cursor before needle becomes cursor before replacement", before: [BEFORE.indexOf(NEEDLE), 0], after: [AFTER.indexOf(REPLACE), 0] - },{ - description: 'cursor at needle start + 1 moves behind replacement', + }, + { + description: "cursor at needle start + 1 moves behind replacement", before: [BEFORE.indexOf(NEEDLE) + 1, 0], after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0] - },{ - description: 'cursor at needle end - 1 stays behind replacement', + }, + { + description: "cursor at needle end - 1 stays behind replacement", before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 0], after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0] - },{ - description: 'cursor behind needle becomes cursor behind replacement', + }, + { + description: "cursor behind needle becomes cursor behind replacement", before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length, 0], after: [AFTER.indexOf(REPLACE) + REPLACE.length, 0] - },{ - description: 'cursor at end remains there', + }, + { + description: "cursor at end remains there", before: [BEFORE.length, 0], after: [AFTER.length, 0] - },{ - description: 'selection spanning needle start becomes selection until replacement start', + }, + { + description: + "selection spanning needle start becomes selection until replacement start", before: [BEFORE.indexOf(NEEDLE) - 1, 2], after: [AFTER.indexOf(REPLACE) - 1, 1] - },{ - description: 'selection spanning needle end becomes selection from replacement end', + }, + { + description: + "selection spanning needle end becomes selection from replacement end", before: [BEFORE.indexOf(NEEDLE) + NEEDLE.length - 1, 2], after: [AFTER.indexOf(REPLACE) + REPLACE.length, 1] - },{ - description: 'selection spanning needle becomes selection spanning replacement', + }, + { + description: + "selection spanning needle becomes selection spanning replacement", before: [BEFORE.indexOf(NEEDLE) - 1, NEEDLE.length + 2], after: [AFTER.indexOf(REPLACE) - 1, REPLACE.length + 2] - },{ - description: 'complete selection remains complete', + }, + { + description: "complete selection remains complete", before: [0, BEFORE.length], after: [0, AFTER.length] } @@ -873,25 +942,33 @@ composerTestCase("replace-text event for composer", function(assert) { return [ '"', text.substr(0, start), - '<', + "<", text.substr(start, len), - '>', - text.substr(start+len), - '"', - ].join(''); + ">", + text.substr(start + len), + '"' + ].join(""); } for (let i = 0; i < CASES.length; i++) { const CASE = CASES[i]; - composerTestCase(`replace-text event: ${CASE.description}`, function(assert, textarea) { - this.set('value', BEFORE); + composerTestCase(`replace-text event: ${CASE.description}`, function( + assert, + textarea + ) { + this.set("value", BEFORE); setSelection(textarea, CASE.before); andThen(() => { - this.container.lookup('app-events:main').trigger('composer:replace-text', 'green', 'yellow'); + this.container + .lookup("app-events:main") + .trigger("composer:replace-text", "green", "yellow"); }); andThen(() => { let expect = formatTextWithSelection(AFTER, CASE.after); - let actual = formatTextWithSelection(this.get('value'), getSelection(textarea)); + let actual = formatTextWithSelection( + this.get("value"), + getSelection(textarea) + ); assert.equal(actual, expect); }); }); diff --git a/test/javascripts/components/d-icon-test.js.es6 b/test/javascripts/components/d-icon-test.js.es6 index d4f3d489aa..bcaca7f753 100644 --- a/test/javascripts/components/d-icon-test.js.es6 +++ b/test/javascripts/components/d-icon-test.js.es6 @@ -1,21 +1,28 @@ -import componentTest from 'helpers/component-test'; +import componentTest from "helpers/component-test"; -moduleForComponent('d-icon', {integration: true}); +moduleForComponent("d-icon", { integration: true }); -componentTest('default', { +componentTest("default", { template: '{{d-icon "bars"}}', test(assert) { - const html = this.$().html().trim(); + const html = this.$() + .html() + .trim(); assert.equal(html, ''); } }); -componentTest('with replacement', { +componentTest("with replacement", { template: '{{d-icon "d-watching"}}', test(assert) { - const html = this.$().html().trim(); - assert.equal(html, ''); + const html = this.$() + .html() + .trim(); + assert.equal( + html, + '' + ); } }); diff --git a/test/javascripts/components/group-membership-button-test.js.es6 b/test/javascripts/components/group-membership-button-test.js.es6 index a321d7ed4f..1c46855150 100644 --- a/test/javascripts/components/group-membership-button-test.js.es6 +++ b/test/javascripts/components/group-membership-button-test.js.es6 @@ -1,67 +1,73 @@ -moduleFor('component:group-membership-button'); +moduleFor("component:group-membership-button"); -QUnit.test('canJoinGroup', function(assert) { +QUnit.test("canJoinGroup", function(assert) { this.subject().setProperties({ model: { public_admission: false, is_group_user: true } }); assert.equal( - this.subject().get("canJoinGroup"), false, + this.subject().get("canJoinGroup"), + false, "can't join group if public_admission is false" ); this.subject().set("model.public_admission", true); assert.equal( - this.subject().get("canJoinGroup"), false, + this.subject().get("canJoinGroup"), + false, "can't join group if user is already in the group" ); this.subject().set("model.is_group_user", false); assert.equal( - this.subject().get("canJoinGroup"), true, + this.subject().get("canJoinGroup"), + true, "allowed to join group" ); }); -QUnit.test('canLeaveGroup', function(assert) { +QUnit.test("canLeaveGroup", function(assert) { this.subject().setProperties({ model: { public_exit: false, is_group_user: false } }); assert.equal( - this.subject().get("canLeaveGroup"), false, + this.subject().get("canLeaveGroup"), + false, "can't leave group if public_exit is false" ); this.subject().set("model.public_exit", true); assert.equal( - this.subject().get("canLeaveGroup"), false, + this.subject().get("canLeaveGroup"), + false, "can't leave group if user is not in the group" ); this.subject().set("model.is_group_user", true); assert.equal( - this.subject().get("canLeaveGroup"), true, + this.subject().get("canLeaveGroup"), + true, "allowed to leave group" ); }); -QUnit.test('userIsGroupUser', function(assert) { +QUnit.test("userIsGroupUser", function(assert) { this.subject().setProperties({ model: { is_group_user: true } }); - assert.equal(this.subject().get('userIsGroupUser'), true); + assert.equal(this.subject().get("userIsGroupUser"), true); - this.subject().set('model.is_group_user', false); + this.subject().set("model.is_group_user", false); - assert.equal(this.subject().get('userIsGroupUser'), false); + assert.equal(this.subject().get("userIsGroupUser"), false); - this.subject().set('model.is_group_user', null); + this.subject().set("model.is_group_user", null); - assert.equal(this.subject().get('userIsGroupUser'), false); + assert.equal(this.subject().get("userIsGroupUser"), false); }); diff --git a/test/javascripts/components/keyboard-shortcuts-test.js.es6 b/test/javascripts/components/keyboard-shortcuts-test.js.es6 index 642f35d543..d127c89fa6 100644 --- a/test/javascripts/components/keyboard-shortcuts-test.js.es6 +++ b/test/javascripts/components/keyboard-shortcuts-test.js.es6 @@ -1,7 +1,7 @@ -import DiscourseURL from 'discourse/lib/url'; +import DiscourseURL from "discourse/lib/url"; var testMouseTrap; -import KeyboardShortcuts from 'discourse/lib/keyboard-shortcuts'; +import KeyboardShortcuts from "discourse/lib/keyboard-shortcuts"; QUnit.module("lib:keyboard-shortcuts", { beforeEach() { @@ -15,8 +15,7 @@ QUnit.module("lib:keyboard-shortcuts", { if (_.isArray(bindings)) { _.each(bindings, registerBinding, this); - } - else { + } else { registerBinding(bindings); } }, @@ -28,38 +27,39 @@ QUnit.module("lib:keyboard-shortcuts", { sandbox.stub(DiscourseURL, "routeTo"); - $("#qunit-fixture").html([ - "
", - "" + - "
", - "
", - "
    ", - "
  • ", - "
  • ", - "
  • ", - "
  • ", - "
", - "
", - "", - " ", - "
", - " ", - "
", - "", - "
", - "", - "
", - "
", - "
", - "
", - "
" - ].join("\n")); + $("#qunit-fixture").html( + [ + "
", + "" + "
", + "
", + "
    ", + "
  • ", + "
  • ", + "
  • ", + "
  • ", + "
", + "
", + "", + " ", + "
", + " ", + "
", + "", + "
", + "", + "
", + "
", + "
", + "
", + "
" + ].join("\n") + ); }, afterEach() { @@ -93,9 +93,13 @@ _.each(clickBindings, function(selector, binding) { assert.ok(true, selector + " was clicked"); }); - _.each(bindings, function(b) { - testMouseTrap.trigger(b); - }, this); + _.each( + bindings, + function(b) { + testMouseTrap.trigger(b); + }, + this + ); }); }); @@ -115,14 +119,14 @@ _.each(functionBindings, function(func, binding) { }); QUnit.test("selectDown calls _moveSelection with 1", assert => { - var spy = sandbox.spy(KeyboardShortcuts, '_moveSelection'); + var spy = sandbox.spy(KeyboardShortcuts, "_moveSelection"); KeyboardShortcuts.selectDown(); assert.ok(spy.calledWith(1), "_moveSelection is called with 1"); }); QUnit.test("selectUp calls _moveSelection with -1", assert => { - var spy = sandbox.spy(KeyboardShortcuts, '_moveSelection'); + var spy = sandbox.spy(KeyboardShortcuts, "_moveSelection"); KeyboardShortcuts.selectUp(); assert.ok(spy.calledWith(-1), "_moveSelection is called with -1"); @@ -130,7 +134,7 @@ QUnit.test("selectUp calls _moveSelection with -1", assert => { QUnit.test("goBack calls history.back", assert => { var called = false; - sandbox.stub(history, 'back', function() { + sandbox.stub(history, "back", function() { called = true; }); @@ -139,14 +143,14 @@ QUnit.test("goBack calls history.back", assert => { }); QUnit.test("nextSection calls _changeSection with 1", assert => { - var spy = sandbox.spy(KeyboardShortcuts, '_changeSection'); + var spy = sandbox.spy(KeyboardShortcuts, "_changeSection"); KeyboardShortcuts.nextSection(); assert.ok(spy.calledWith(1), "_changeSection is called with 1"); }); QUnit.test("prevSection calls _changeSection with -1", assert => { - var spy = sandbox.spy(KeyboardShortcuts, '_changeSection'); + var spy = sandbox.spy(KeyboardShortcuts, "_changeSection"); KeyboardShortcuts.prevSection(); assert.ok(spy.calledWith(-1), "_changeSection is called with -1"); diff --git a/test/javascripts/components/list-setting-test.js.es6 b/test/javascripts/components/list-setting-test.js.es6 index d45e02429c..a69d17a8f9 100644 --- a/test/javascripts/components/list-setting-test.js.es6 +++ b/test/javascripts/components/list-setting-test.js.es6 @@ -1,85 +1,108 @@ -import componentTest from 'helpers/component-test'; +import componentTest from "helpers/component-test"; -moduleForComponent('list-setting', {integration: true}); +moduleForComponent("list-setting", { integration: true }); -componentTest('default', { - template: '{{list-setting settingValue=settingValue choices=choices}}', +componentTest("default", { + template: "{{list-setting settingValue=settingValue choices=choices}}", beforeEach() { - this.set('settingValue', 'bold|italic'); - this.set('choices', ['bold', 'italic', 'underline']); + this.set("settingValue", "bold|italic"); + this.set("choices", ["bold", "italic", "underline"]); }, test(assert) { andThen(() => { - assert.equal(selectKit().header().title(), 'bold,italic'); - assert.equal(selectKit().header().value(), 'bold,italic'); + assert.equal( + selectKit() + .header() + .title(), + "bold,italic" + ); + assert.equal( + selectKit() + .header() + .value(), + "bold,italic" + ); }); } }); -componentTest('with empty string as value', { - template: '{{list-setting settingValue=settingValue}}', +componentTest("with empty string as value", { + template: "{{list-setting settingValue=settingValue}}", beforeEach() { - this.set('settingValue', ''); + this.set("settingValue", ""); }, test(assert) { andThen(() => { - assert.equal(selectKit().header().value(), ""); + assert.equal( + selectKit() + .header() + .value(), + "" + ); }); } }); -componentTest('with only setting value', { - template: '{{list-setting settingValue=settingValue}}', +componentTest("with only setting value", { + template: "{{list-setting settingValue=settingValue}}", beforeEach() { - this.set('settingValue', 'bold|italic'); + this.set("settingValue", "bold|italic"); }, test(assert) { andThen(() => { - assert.equal(selectKit().header().value(), 'bold,italic'); + assert.equal( + selectKit() + .header() + .value(), + "bold,italic" + ); }); } }); -componentTest('interactions', { - template: '{{list-setting settingValue=settingValue choices=choices}}', +componentTest("interactions", { + template: "{{list-setting settingValue=settingValue choices=choices}}", beforeEach() { - this.set('settingValue', 'bold|italic'); - this.set('choices', ['bold', 'italic', 'underline']); + this.set("settingValue", "bold|italic"); + this.set("choices", ["bold", "italic", "underline"]); }, test(assert) { const listSetting = selectKit(); - listSetting.expand().selectRowByValue('underline'); + listSetting.expand().selectRowByValue("underline"); andThen(() => { - assert.equal(listSetting.header().value(), 'bold,italic,underline'); + assert.equal(listSetting.header().value(), "bold,italic,underline"); }); - listSetting.expand().fillInFilter('strike'); + listSetting.expand().fillInFilter("strike"); andThen(() => { - assert.equal(listSetting.highlightedRow().value(), 'strike'); + assert.equal(listSetting.highlightedRow().value(), "strike"); }); listSetting.keyboard().enter(); andThen(() => { - assert.equal(listSetting.header().value(), 'bold,italic,underline,strike'); + assert.equal( + listSetting.header().value(), + "bold,italic,underline,strike" + ); }); listSetting.keyboard().backspace(); listSetting.keyboard().backspace(); andThen(() => { - assert.equal(listSetting.header().value(), 'bold,italic,underline'); + assert.equal(listSetting.header().value(), "bold,italic,underline"); }); } }); diff --git a/test/javascripts/components/multi-select-test.js.es6 b/test/javascripts/components/multi-select-test.js.es6 index c829b3ea53..cf37448d71 100644 --- a/test/javascripts/components/multi-select-test.js.es6 +++ b/test/javascripts/components/multi-select-test.js.es6 @@ -1,203 +1,301 @@ -import componentTest from 'helpers/component-test'; -moduleForComponent('multi-select', { +import componentTest from "helpers/component-test"; +moduleForComponent("multi-select", { integration: true, beforeEach: function() { - this.set('subject', selectKit()); + this.set("subject", selectKit()); } }); -componentTest('with objects and values', { - template: '{{multi-select content=items values=values}}', +componentTest("with objects and values", { + template: "{{multi-select content=items values=values}}", beforeEach() { - this.set('items', [{id: 1, name: 'hello'}, {id: 2, name: 'world'}]); - this.set('values', [1, 2]); + this.set("items", [{ id: 1, name: "hello" }, { id: 2, name: "world" }]); + this.set("values", [1, 2]); }, test(assert) { andThen(() => { - assert.equal(this.get('subject').header().value(), '1,2'); + assert.equal( + this.get("subject") + .header() + .value(), + "1,2" + ); }); } }); -componentTest('with title', { +componentTest("with title", { template: '{{multi-select title=(i18n "test.title")}}', beforeEach() { - I18n.translations[I18n.locale].js.test = {title: 'My title'}; + I18n.translations[I18n.locale].js.test = { title: "My title" }; }, test(assert) { - andThen(() => assert.equal(selectKit().header().title(), 'My title') ); + andThen(() => + assert.equal( + selectKit() + .header() + .title(), + "My title" + ) + ); } }); - -componentTest('interactions', { - template: '{{multi-select none=none content=items values=values}}', +componentTest("interactions", { + template: "{{multi-select none=none content=items values=values}}", beforeEach() { - I18n.translations[I18n.locale].js.test = {none: 'none'}; - this.set('items', [{id: 1, name: 'regis'}, {id: 2, name: 'sam'}, {id: 3, name: 'robin'}]); - this.set('values', [1, 2]); + I18n.translations[I18n.locale].js.test = { none: "none" }; + this.set("items", [ + { id: 1, name: "regis" }, + { id: 2, name: "sam" }, + { id: 3, name: "robin" } + ]); + this.set("values", [1, 2]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { assert.equal( - this.get('subject').highlightedRow().name(), - 'robin', - 'it highlights the first content row' + this.get("subject") + .highlightedRow() + .name(), + "robin", + "it highlights the first content row" ); }); - this.set('none', 'test.none'); + this.set("none", "test.none"); andThen(() => { - assert.ok(this.get('subject').noneRow().exists()); + assert.ok( + this.get("subject") + .noneRow() + .exists() + ); assert.equal( - this.get('subject').highlightedRow().name(), - 'robin', - 'it highlights the first content row' + this.get("subject") + .highlightedRow() + .name(), + "robin", + "it highlights the first content row" ); }); - this.get('subject').selectRowByValue(3); - this.get('subject').expand(); + this.get("subject").selectRowByValue(3); + this.get("subject").expand(); andThen(() => { assert.equal( - this.get('subject').highlightedRow().name(), - 'none', - 'it highlights none row if no content' + this.get("subject") + .highlightedRow() + .name(), + "none", + "it highlights none row if no content" ); }); - this.get('subject').fillInFilter('joffrey'); + this.get("subject").fillInFilter("joffrey"); andThen(() => { assert.equal( - this.get('subject').highlightedRow().name(), - 'joffrey', - 'it highlights create row when filling filter' + this.get("subject") + .highlightedRow() + .name(), + "joffrey", + "it highlights create row when filling filter" ); }); - this.get('subject').keyboard().enter(); + this.get("subject") + .keyboard() + .enter(); andThen(() => { assert.equal( - this.get('subject').highlightedRow().name(), - 'none', - 'it highlights none row after creating content and no content left' + this.get("subject") + .highlightedRow() + .name(), + "none", + "it highlights none row after creating content and no content left" ); }); - this.get('subject').keyboard().backspace(); + this.get("subject") + .keyboard() + .backspace(); andThen(() => { - const $lastSelectedName = this.get('subject').header().el().find('.selected-name').last(); - assert.equal($lastSelectedName.attr('data-name'), 'joffrey'); - assert.ok($lastSelectedName.hasClass('is-highlighted'), 'it highlights the last selected name when using backspace'); + const $lastSelectedName = this.get("subject") + .header() + .el() + .find(".selected-name") + .last(); + assert.equal($lastSelectedName.attr("data-name"), "joffrey"); + assert.ok( + $lastSelectedName.hasClass("is-highlighted"), + "it highlights the last selected name when using backspace" + ); }); - this.get('subject').keyboard().backspace(); + this.get("subject") + .keyboard() + .backspace(); andThen(() => { - const $lastSelectedName = this.get('subject').header().el().find('.selected-name').last(); - assert.equal($lastSelectedName.attr('data-name'), 'robin', 'it removes the previous highlighted selected content'); - assert.notOk(this.get('subject').rowByValue('joffrey').exists(), 'generated content shouldn’t appear in content when removed'); + const $lastSelectedName = this.get("subject") + .header() + .el() + .find(".selected-name") + .last(); + assert.equal( + $lastSelectedName.attr("data-name"), + "robin", + "it removes the previous highlighted selected content" + ); + assert.notOk( + this.get("subject") + .rowByValue("joffrey") + .exists(), + "generated content shouldn’t appear in content when removed" + ); }); - this.get('subject').keyboard().selectAll(); + this.get("subject") + .keyboard() + .selectAll(); andThen(() => { - const $highlightedSelectedNames = this.get('subject').header().el().find('.selected-name.is-highlighted'); - assert.equal($highlightedSelectedNames.length, 3, 'it highlights each selected name'); + const $highlightedSelectedNames = this.get("subject") + .header() + .el() + .find(".selected-name.is-highlighted"); + assert.equal( + $highlightedSelectedNames.length, + 3, + "it highlights each selected name" + ); }); - this.get('subject').keyboard().backspace(); + this.get("subject") + .keyboard() + .backspace(); andThen(() => { - const $selectedNames = this.get('subject').header().el().find('.selected-name'); - assert.equal($selectedNames.length, 0, 'it removed all selected content'); + const $selectedNames = this.get("subject") + .header() + .el() + .find(".selected-name"); + assert.equal($selectedNames.length, 0, "it removed all selected content"); }); andThen(() => { - assert.ok(this.get('subject').isFocused()); - assert.ok(this.get('subject').isExpanded()); + assert.ok(this.get("subject").isFocused()); + assert.ok(this.get("subject").isExpanded()); }); - this.get('subject').keyboard().escape(); + this.get("subject") + .keyboard() + .escape(); andThen(() => { - assert.ok(this.get('subject').isFocused()); - assert.notOk(this.get('subject').isExpanded()); + assert.ok(this.get("subject").isFocused()); + assert.notOk(this.get("subject").isExpanded()); }); - this.get('subject').keyboard().escape(); + this.get("subject") + .keyboard() + .escape(); andThen(() => { - assert.notOk(this.get('subject').isFocused()); - assert.notOk(this.get('subject').isExpanded()); + assert.notOk(this.get("subject").isFocused()); + assert.notOk(this.get("subject").isExpanded()); }); } }); -componentTest('with limitMatches', { - template: '{{multi-select content=content limitMatches=2}}', +componentTest("with limitMatches", { + template: "{{multi-select content=content limitMatches=2}}", beforeEach() { - this.set('content', ['sam', 'jeff', 'neil']); + this.set("content", ["sam", "jeff", "neil"]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').el().find(".select-kit-row").length, 2)); + andThen(() => + assert.equal( + this.get("subject") + .el() + .find(".select-kit-row").length, + 2 + ) + ); } }); -componentTest('with minimum', { - template: '{{multi-select content=content minimum=1}}', +componentTest("with minimum", { + template: "{{multi-select content=content minimum=1}}", beforeEach() { - this.set('content', ['sam', 'jeff', 'neil']); + this.set("content", ["sam", "jeff", "neil"]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').validationMessage(), 'Select at least 1 item.')); + andThen(() => + assert.equal( + this.get("subject").validationMessage(), + "Select at least 1 item." + ) + ); - this.get('subject').selectRowByValue('sam'); + this.get("subject").selectRowByValue("sam"); andThen(() => { - assert.equal(this.get('subject').header().label(), 'sam'); + assert.equal( + this.get("subject") + .header() + .label(), + "sam" + ); }); } }); -componentTest('with minimumLabel', { - template: '{{multi-select content=content minimum=1 minimumLabel="test.minimum"}}', +componentTest("with minimumLabel", { + template: + '{{multi-select content=content minimum=1 minimumLabel="test.minimum"}}', beforeEach() { - I18n.translations[I18n.locale].js.test = { minimum: 'min %{count}' }; - this.set('content', ['sam', 'jeff', 'neil']); + I18n.translations[I18n.locale].js.test = { minimum: "min %{count}" }; + this.set("content", ["sam", "jeff", "neil"]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').validationMessage(), 'min 1')); + andThen(() => + assert.equal(this.get("subject").validationMessage(), "min 1") + ); - this.get('subject').selectRowByValue('jeff'); + this.get("subject").selectRowByValue("jeff"); andThen(() => { - assert.equal(this.get('subject').header().label(), 'jeff'); + assert.equal( + this.get("subject") + .header() + .label(), + "jeff" + ); }); } }); diff --git a/test/javascripts/components/pinned-options-test.js.es6 b/test/javascripts/components/pinned-options-test.js.es6 index 87aafc07d2..c195ad5a8e 100644 --- a/test/javascripts/components/pinned-options-test.js.es6 +++ b/test/javascripts/components/pinned-options-test.js.es6 @@ -1,5 +1,5 @@ -import componentTest from 'helpers/component-test'; -import Topic from 'discourse/models/topic'; +import componentTest from "helpers/component-test"; +import Topic from "discourse/models/topic"; const buildTopic = function() { return Topic.create({ @@ -10,15 +10,15 @@ const buildTopic = function() { }); }; -moduleForComponent('pinned-options', { +moduleForComponent("pinned-options", { integration: true, beforeEach: function() { - this.set('subject', selectKit()); + this.set("subject", selectKit()); } }); -componentTest('updating the content refreshes the list', { - template: '{{pinned-options value=pinned topic=topic}}', +componentTest("updating the content refreshes the list", { + template: "{{pinned-options value=pinned topic=topic}}", beforeEach() { this.siteSettings.automatically_unpin_topics = false; @@ -28,13 +28,23 @@ componentTest('updating the content refreshes the list', { test(assert) { andThen(() => { - assert.equal(this.get('subject').header().name(), "pinned"); + assert.equal( + this.get("subject") + .header() + .name(), + "pinned" + ); }); andThen(() => this.set("pinned", false)); andThen(() => { - assert.equal(this.get('subject').header().name(), "unpinned"); + assert.equal( + this.get("subject") + .header() + .name(), + "unpinned" + ); }); } }); diff --git a/test/javascripts/components/share-button-test.js.es6 b/test/javascripts/components/share-button-test.js.es6 index 41d0fa8481..95861c305a 100644 --- a/test/javascripts/components/share-button-test.js.es6 +++ b/test/javascripts/components/share-button-test.js.es6 @@ -1,16 +1,15 @@ -import componentTest from 'helpers/component-test'; -moduleForComponent('share-button', {integration: true}); +import componentTest from "helpers/component-test"; +moduleForComponent("share-button", { integration: true }); -componentTest('share button', { +componentTest("share button", { template: '{{share-button url="https://eviltrout.com"}}', test(assert) { - assert.ok(this.$(`button.share`).length, 'it has all the classes'); + assert.ok(this.$(`button.share`).length, "it has all the classes"); assert.ok( this.$(`button[data-share-url="https://eviltrout.com"]`).length, - 'it has the data attribute for sharing' + "it has the data attribute for sharing" ); } }); - diff --git a/test/javascripts/components/single-select-test.js.es6 b/test/javascripts/components/single-select-test.js.es6 index 671896bc18..b436a64897 100644 --- a/test/javascripts/components/single-select-test.js.es6 +++ b/test/javascripts/components/single-select-test.js.es6 @@ -1,26 +1,31 @@ -import componentTest from 'helpers/component-test'; -import { withPluginApi } from 'discourse/lib/plugin-api'; -import { clearCallbacks } from 'select-kit/mixins/plugin-api'; +import componentTest from "helpers/component-test"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import { clearCallbacks } from "select-kit/mixins/plugin-api"; -moduleForComponent('single-select', { +moduleForComponent("single-select", { integration: true, beforeEach: function() { - this.set('subject', selectKit()); + this.set("subject", selectKit()); } }); -componentTest('updating the content refreshes the list', { - template: '{{single-select value=1 content=content}}', +componentTest("updating the content refreshes the list", { + template: "{{single-select value=1 content=content}}", beforeEach() { this.set("content", [{ id: 1, name: "BEFORE" }]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').rowByValue(1).name(), "BEFORE"); + assert.equal( + this.get("subject") + .rowByValue(1) + .name(), + "BEFORE" + ); }); andThen(() => { @@ -28,13 +33,18 @@ componentTest('updating the content refreshes the list', { }); andThen(() => { - assert.equal(this.get('subject').rowByValue(1).name(), "AFTER"); + assert.equal( + this.get("subject") + .rowByValue(1) + .name(), + "AFTER" + ); }); } }); -componentTest('accepts a value by reference', { - template: '{{single-select value=value content=content}}', +componentTest("accepts a value by reference", { + template: "{{single-select value=value content=content}}", beforeEach() { this.set("value", 1); @@ -42,16 +52,19 @@ componentTest('accepts a value by reference', { }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { assert.equal( - this.get('subject').selectedRow().name(), "robin", + this.get("subject") + .selectedRow() + .name(), + "robin", "it highlights the row corresponding to the value" ); }); - this.get('subject').selectRowByValue(1); + this.get("subject").selectRowByValue(1); andThen(() => { assert.equal(this.get("value"), 1, "it mutates the value"); @@ -59,72 +72,92 @@ componentTest('accepts a value by reference', { } }); -componentTest('no default icon', { - template: '{{single-select}}', +componentTest("no default icon", { + template: "{{single-select}}", test(assert) { assert.equal( - this.get('subject').header().icon().length, + this.get("subject") + .header() + .icon().length, 0, "it doesn’t have an icon if not specified" ); } }); -componentTest('default search icon', { - template: '{{single-select filterable=true}}', +componentTest("default search icon", { + template: "{{single-select filterable=true}}", test(assert) { - this.get('subject').expand(); - - andThen(() => { - assert.ok(exists(this.get('subject').filter().icon()), "it has an icon"); - }); - } -}); - -componentTest('with no search icon', { - template: '{{single-select filterable=true filterIcon=null}}', - - test(assert) { - this.get('subject').expand(); - - andThen(() => { - assert.notOk(exists(this.get('subject').filter().icon()), "it has no icon"); - }); - } -}); - -componentTest('custom search icon', { - template: '{{single-select filterable=true filterIcon="shower"}}', - - test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { assert.ok( - this.get('subject').filter().icon().hasClass("d-icon-shower"), + exists( + this.get("subject") + .filter() + .icon() + ), + "it has an icon" + ); + }); + } +}); + +componentTest("with no search icon", { + template: "{{single-select filterable=true filterIcon=null}}", + + test(assert) { + this.get("subject").expand(); + + andThen(() => { + assert.notOk( + exists( + this.get("subject") + .filter() + .icon() + ), + "it has no icon" + ); + }); + } +}); + +componentTest("custom search icon", { + template: '{{single-select filterable=true filterIcon="shower"}}', + + test(assert) { + this.get("subject").expand(); + + andThen(() => { + assert.ok( + this.get("subject") + .filter() + .icon() + .hasClass("d-icon-shower"), "it has a the correct icon" ); }); } }); -componentTest('is expandable', { - template: '{{single-select}}', +componentTest("is expandable", { + template: "{{single-select}}", test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.ok(this.get('subject').isExpanded()) ); + andThen(() => assert.ok(this.get("subject").isExpanded())); - this.get('subject').collapse(); + this.get("subject").collapse(); - andThen(() => assert.notOk(this.get('subject').isExpanded()) ); + andThen(() => assert.notOk(this.get("subject").isExpanded())); } }); -componentTest('accepts custom value/name keys', { - template: '{{single-select value=value nameProperty="item" content=content valueAttribute="identifier"}}', +componentTest("accepts custom value/name keys", { + template: + '{{single-select value=value nameProperty="item" content=content valueAttribute="identifier"}}', beforeEach() { this.set("value", 1); @@ -132,16 +165,21 @@ componentTest('accepts custom value/name keys', { }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').selectedRow().name(), "robin"); + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "robin" + ); }); } }); -componentTest('doesn’t render collection content before first expand', { - template: '{{single-select value=1 content=content}}', +componentTest("doesn’t render collection content before first expand", { + template: "{{single-select value=1 content=content}}", beforeEach() { this.set("content", [{ value: 1, name: "robin" }]); @@ -150,7 +188,7 @@ componentTest('doesn’t render collection content before first expand', { test(assert) { assert.notOk(exists(find(".select-kit-collection"))); - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { assert.ok(exists(find(".select-kit-collection"))); @@ -158,34 +196,41 @@ componentTest('doesn’t render collection content before first expand', { } }); -componentTest('dynamic headerText', { - template: '{{single-select value=1 content=content}}', +componentTest("dynamic headerText", { + template: "{{single-select value=1 content=content}}", beforeEach() { this.set("content", [{ id: 1, name: "robin" }, { id: 2, name: "regis" }]); }, test(assert) { - this.get('subject').expand(); - - andThen(() => { - assert.equal(this.get('subject').header().name(), 'robin'); - }); - - this.get('subject').selectRowByValue(2); + this.get("subject").expand(); andThen(() => { assert.equal( - this.get('subject').header().name(), - 'regis', - 'it changes header text' + this.get("subject") + .header() + .name(), + "robin" + ); + }); + + this.get("subject").selectRowByValue(2); + + andThen(() => { + assert.equal( + this.get("subject") + .header() + .name(), + "regis", + "it changes header text" ); }); } }); -componentTest('supports custom row template', { - template: '{{single-select content=content templateForRow=templateForRow}}', +componentTest("supports custom row template", { + template: "{{single-select content=content templateForRow=templateForRow}}", beforeEach() { this.set("content", [{ id: 1, name: "robin" }]); @@ -195,224 +240,331 @@ componentTest('supports custom row template', { }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { assert.equal( - this.get('subject').rowByValue(1).el().html().trim(), "robin" + this.get("subject") + .rowByValue(1) + .el() + .html() + .trim(), + "robin" ); }); } }); -componentTest('supports converting select value to integer', { - template: '{{single-select value=value content=content castInteger=true}}', +componentTest("supports converting select value to integer", { + template: "{{single-select value=value content=content castInteger=true}}", beforeEach() { - this.set('value', 2); - this.set('content', [{ id: '1', name: 'robin'}, {id: '2', name: 'régis' }]); + this.set("value", 2); + this.set("content", [ + { id: "1", name: "robin" }, + { id: "2", name: "régis" } + ]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').selectedRow().name(), 'régis') ); + andThen(() => + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "régis" + ) + ); andThen(() => { - this.set('value', 1); + this.set("value", 1); }); andThen(() => { assert.equal( - this.get('subject').selectedRow().name(), - 'robin', - 'it works with dynamic content' + this.get("subject") + .selectedRow() + .name(), + "robin", + "it works with dynamic content" ); }); } }); -componentTest('supports converting string as boolean to boolean', { - template: '{{single-select value=value content=content castBoolean=true}}', +componentTest("supports converting string as boolean to boolean", { + template: "{{single-select value=value content=content castBoolean=true}}", beforeEach() { - this.set('value', true); - this.set('content', [{ id: 'true', name: 'ASC'}, {id: 'false', name: 'DESC' }]); + this.set("value", true); + this.set("content", [ + { id: "true", name: "ASC" }, + { id: "false", name: "DESC" } + ]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').selectedRow().name(), 'ASC') ); + andThen(() => + assert.equal( + this.get("subject") + .selectedRow() + .name(), + "ASC" + ) + ); andThen(() => { - this.set('value', false); + this.set("value", false); }); andThen(() => { assert.equal( - this.get('subject').selectedRow().name(), - 'DESC', - 'it works with dynamic content' + this.get("subject") + .selectedRow() + .name(), + "DESC", + "it works with dynamic content" ); }); } }); -componentTest('supports keyboard events', { - template: '{{single-select content=content filterable=true}}', +componentTest("supports keyboard events", { + template: "{{single-select content=content filterable=true}}", beforeEach() { this.set("content", [{ id: 1, name: "robin" }, { id: 2, name: "regis" }]); }, test(assert) { - this.get('subject').expand().keyboard().down(); + this.get("subject") + .expand() + .keyboard() + .down(); andThen(() => { - assert.equal(this.get('subject').highlightedRow().title(), "regis", "the next row is highlighted"); + assert.equal( + this.get("subject") + .highlightedRow() + .title(), + "regis", + "the next row is highlighted" + ); }); - this.get('subject').keyboard().down(); + this.get("subject") + .keyboard() + .down(); andThen(() => { - assert.equal(this.get('subject').highlightedRow().title(), "robin", "it returns to the first row"); + assert.equal( + this.get("subject") + .highlightedRow() + .title(), + "robin", + "it returns to the first row" + ); }); - this.get('subject').keyboard().up(); + this.get("subject") + .keyboard() + .up(); andThen(() => { - assert.equal(this.get('subject').highlightedRow().title(), "regis", "it highlights the last row"); + assert.equal( + this.get("subject") + .highlightedRow() + .title(), + "regis", + "it highlights the last row" + ); }); - this.get('subject').keyboard().enter(); + this.get("subject") + .keyboard() + .enter(); andThen(() => { - assert.equal(this.get('subject').selectedRow().title(), "regis", "it selects the row when pressing enter"); - assert.notOk(this.get('subject').isExpanded(), "it collapses the select box when selecting a row"); + assert.equal( + this.get("subject") + .selectedRow() + .title(), + "regis", + "it selects the row when pressing enter" + ); + assert.notOk( + this.get("subject").isExpanded(), + "it collapses the select box when selecting a row" + ); }); - this.get('subject').expand().keyboard().escape(); + this.get("subject") + .expand() + .keyboard() + .escape(); andThen(() => { - assert.notOk(this.get('subject').isExpanded(), "it collapses the select box"); + assert.notOk( + this.get("subject").isExpanded(), + "it collapses the select box" + ); }); - this.get('subject').expand().fillInFilter('regis').keyboard().tab(); + this.get("subject") + .expand() + .fillInFilter("regis") + .keyboard() + .tab(); andThen(() => { - assert.notOk(this.get('subject').isExpanded(), "it collapses the select box when selecting a row"); + assert.notOk( + this.get("subject").isExpanded(), + "it collapses the select box when selecting a row" + ); }); } }); - -componentTest('with allowInitialValueMutation', { - template: '{{single-select value=value content=content allowInitialValueMutation=true}}', +componentTest("with allowInitialValueMutation", { + template: + "{{single-select value=value content=content allowInitialValueMutation=true}}", beforeEach() { this.set("value", ""); - this.set("content", [{ id: "1", name: "robin"}, {id: "2", name: "régis" }]); + this.set("content", [ + { id: "1", name: "robin" }, + { id: "2", name: "régis" } + ]); }, test(assert) { andThen(() => { - assert.equal(this.get("value"), "1", "it mutates the value on initial rendering"); + assert.equal( + this.get("value"), + "1", + "it mutates the value on initial rendering" + ); }); } }); -componentTest('support appending content through plugin api', { - template: '{{single-select content=content}}', +componentTest("support appending content through plugin api", { + template: "{{single-select content=content}}", beforeEach() { - withPluginApi('0.8.13', api => { - api.modifySelectKit('select-kit') - .appendContent([{ id: '2', name: 'regis'}]); - }); - - this.set('content', [{ id: '1', name: 'robin'}]); - }, - test(assert) { - this.get('subject').expand(); - - andThen(() => { - assert.equal(this.get('subject').rows().length, 2); - assert.equal(this.get('subject').rowByIndex(1).name(), 'regis'); - }); - - andThen(() => clearCallbacks()); - } -}); - -componentTest('support modifying content through plugin api', { - template: '{{single-select content=content}}', - - beforeEach() { - withPluginApi('0.8.13', api => { - api.modifySelectKit("select-kit") - .modifyContent((context, existingContent) => { - existingContent.splice(1, 0, { id: "2", name: "sam" }); - return existingContent; - }); - }); - - this.set("content", [{ id: "1", name: "robin"}, { id: "3", name: "regis"}]); - }, - - test(assert) { - this.get('subject').expand(); - - andThen(() => { - assert.equal(this.get('subject').rows().length, 3); - assert.equal(this.get('subject').rowByIndex(1).name(), "sam"); - }); - - andThen(() => clearCallbacks()); - } -}); - -componentTest('support prepending content through plugin api', { - template: '{{single-select content=content}}', - - beforeEach() { - withPluginApi('0.8.13', api => { - api.modifySelectKit("select-kit") - .prependContent([{ id: "2", name: "regis"}]); - }); - - this.set("content", [{ id: "1", name: "robin"}]); - }, - - test(assert) { - this.get('subject').expand(); - - andThen(() => { - assert.equal(this.get('subject').rows().length, 2); - assert.equal(this.get('subject').rowByIndex(0).name(), "regis"); - }); - - andThen(() => clearCallbacks()); - } -}); - -componentTest('support modifying on select behavior through plugin api', { - template: '{{single-select content=content}}', - - beforeEach() { - withPluginApi('0.8.13', api => { + withPluginApi("0.8.13", api => { api .modifySelectKit("select-kit") - .onSelect((context, value) => { - find(".on-select-test").html(value); + .appendContent([{ id: "2", name: "regis" }]); + }); + + this.set("content", [{ id: "1", name: "robin" }]); + }, + test(assert) { + this.get("subject").expand(); + + andThen(() => { + assert.equal(this.get("subject").rows().length, 2); + assert.equal( + this.get("subject") + .rowByIndex(1) + .name(), + "regis" + ); + }); + + andThen(() => clearCallbacks()); + } +}); + +componentTest("support modifying content through plugin api", { + template: "{{single-select content=content}}", + + beforeEach() { + withPluginApi("0.8.13", api => { + api + .modifySelectKit("select-kit") + .modifyContent((context, existingContent) => { + existingContent.splice(1, 0, { id: "2", name: "sam" }); + return existingContent; }); }); - this.set("content", [{ id: "1", name: "robin"}]); + this.set("content", [ + { id: "1", name: "robin" }, + { id: "3", name: "regis" } + ]); }, test(assert) { - this.get('subject').expand().selectRowByValue(1); + this.get("subject").expand(); + + andThen(() => { + assert.equal(this.get("subject").rows().length, 3); + assert.equal( + this.get("subject") + .rowByIndex(1) + .name(), + "sam" + ); + }); + + andThen(() => clearCallbacks()); + } +}); + +componentTest("support prepending content through plugin api", { + template: "{{single-select content=content}}", + + beforeEach() { + withPluginApi("0.8.13", api => { + api + .modifySelectKit("select-kit") + .prependContent([{ id: "2", name: "regis" }]); + }); + + this.set("content", [{ id: "1", name: "robin" }]); + }, + + test(assert) { + this.get("subject").expand(); + + andThen(() => { + assert.equal(this.get("subject").rows().length, 2); + assert.equal( + this.get("subject") + .rowByIndex(0) + .name(), + "regis" + ); + }); + + andThen(() => clearCallbacks()); + } +}); + +componentTest("support modifying on select behavior through plugin api", { + template: + '{{single-select content=content}}', + + beforeEach() { + withPluginApi("0.8.13", api => { + api.modifySelectKit("select-kit").onSelect((context, value) => { + find(".on-select-test").html(value); + }); + }); + + this.set("content", [{ id: "1", name: "robin" }]); + }, + + test(assert) { + this.get("subject") + .expand() + .selectRowByValue(1); andThen(() => { assert.equal(find(".on-select-test").html(), "1"); @@ -422,19 +574,24 @@ componentTest('support modifying on select behavior through plugin api', { } }); -componentTest('with nameChanges', { - template: '{{single-select content=content nameChanges=true}}', +componentTest("with nameChanges", { + template: "{{single-select content=content nameChanges=true}}", beforeEach() { - this.set("robin", { id: "1", name: "robin"}); + this.set("robin", { id: "1", name: "robin" }); this.set("content", [this.get("robin")]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), "robin"); + assert.equal( + this.get("subject") + .header() + .name(), + "robin" + ); }); andThen(() => { @@ -442,130 +599,183 @@ componentTest('with nameChanges', { }); andThen(() => { - assert.equal(this.get('subject').header().name(), "robin2"); + assert.equal( + this.get("subject") + .header() + .name(), + "robin2" + ); }); } }); - -componentTest('with null value', { - template: '{{single-select content=content}}', +componentTest("with null value", { + template: "{{single-select content=content}}", beforeEach() { this.set("content", [{ name: "robin" }]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().name(), "robin"); - assert.equal(this.get('subject').header().value(), undefined); + assert.equal( + this.get("subject") + .header() + .name(), + "robin" + ); + assert.equal( + this.get("subject") + .header() + .value(), + undefined + ); }); } }); -componentTest('with collection header', { - template: '{{single-select collectionHeader=collectionHeader}}', +componentTest("with collection header", { + template: "{{single-select collectionHeader=collectionHeader}}", beforeEach() { this.set("collectionHeader", "

Hello

"); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => assert.ok(exists(".collection-header h2"))); } }); -componentTest('with title', { +componentTest("with title", { template: '{{single-select title=(i18n "test.title")}}', beforeEach() { - I18n.translations[I18n.locale].js.test = {title: 'My title'}; + I18n.translations[I18n.locale].js.test = { title: "My title" }; }, test(assert) { - andThen(() => assert.equal(this.get('subject').header().title(), 'My title') ); + andThen(() => + assert.equal( + this.get("subject") + .header() + .title(), + "My title" + ) + ); } }); -componentTest('support modifying header computed content through plugin api', { - template: '{{single-select content=content}}', +componentTest("support modifying header computed content through plugin api", { + template: "{{single-select content=content}}", beforeEach() { - withPluginApi('0.8.15', api => { - api.modifySelectKit("select-kit") + withPluginApi("0.8.15", api => { + api + .modifySelectKit("select-kit") .modifyHeaderComputedContent((context, computedContent) => { computedContent.title = "Not so evil"; return computedContent; }); }); - this.set("content", [{ id: "1", name: "robin"}]); + this.set("content", [{ id: "1", name: "robin" }]); }, test(assert) { andThen(() => { - assert.equal(this.get('subject').header().title(), "Not so evil"); + assert.equal( + this.get("subject") + .header() + .title(), + "Not so evil" + ); }); andThen(() => clearCallbacks()); } }); -componentTest('with limitMatches', { - template: '{{single-select content=content limitMatches=2}}', +componentTest("with limitMatches", { + template: "{{single-select content=content limitMatches=2}}", beforeEach() { - this.set('content', ['sam', 'jeff', 'neil']); + this.set("content", ["sam", "jeff", "neil"]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').el().find(".select-kit-row").length, 2)); + andThen(() => + assert.equal( + this.get("subject") + .el() + .find(".select-kit-row").length, + 2 + ) + ); } }); -componentTest('with minimum', { - template: '{{single-select content=content minimum=1 allowAutoSelectFirst=false}}', +componentTest("with minimum", { + template: + "{{single-select content=content minimum=1 allowAutoSelectFirst=false}}", beforeEach() { - this.set('content', ['sam', 'jeff', 'neil']); + this.set("content", ["sam", "jeff", "neil"]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').validationMessage(), 'Select at least 1 item.')); + andThen(() => + assert.equal( + this.get("subject").validationMessage(), + "Select at least 1 item." + ) + ); - this.get('subject').selectRowByValue('sam'); + this.get("subject").selectRowByValue("sam"); andThen(() => { - assert.equal(this.get('subject').header().label(), 'sam'); + assert.equal( + this.get("subject") + .header() + .label(), + "sam" + ); }); } }); -componentTest('with minimumLabel', { - template: '{{single-select content=content minimum=1 minimumLabel="test.minimum" allowAutoSelectFirst=false}}', +componentTest("with minimumLabel", { + template: + '{{single-select content=content minimum=1 minimumLabel="test.minimum" allowAutoSelectFirst=false}}', beforeEach() { - I18n.translations[I18n.locale].js.test = { minimum: 'min %{count}' }; - this.set('content', ['sam', 'jeff', 'neil']); + I18n.translations[I18n.locale].js.test = { minimum: "min %{count}" }; + this.set("content", ["sam", "jeff", "neil"]); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); - andThen(() => assert.equal(this.get('subject').validationMessage(), 'min 1')); + andThen(() => + assert.equal(this.get("subject").validationMessage(), "min 1") + ); - this.get('subject').selectRowByValue('jeff'); + this.get("subject").selectRowByValue("jeff"); andThen(() => { - assert.equal(this.get('subject').header().label(), 'jeff'); + assert.equal( + this.get("subject") + .header() + .label(), + "jeff" + ); }); } }); diff --git a/test/javascripts/components/text-field-test.js.es6 b/test/javascripts/components/text-field-test.js.es6 index 7a2de69a5a..6535c92397 100644 --- a/test/javascripts/components/text-field-test.js.es6 +++ b/test/javascripts/components/text-field-test.js.es6 @@ -1,4 +1,4 @@ -import componentTest from 'helpers/component-test'; +import componentTest from "helpers/component-test"; moduleForComponent("text-field", { integration: true }); @@ -6,7 +6,7 @@ componentTest("renders correctly with no properties set", { template: `{{text-field}}`, test(assert) { - assert.ok(this.$('input[type=text]').length); + assert.ok(this.$("input[type=text]").length); } }); @@ -18,8 +18,8 @@ componentTest("support a placeholder", { }, test(assert) { - assert.ok(this.$('input[type=text]').length); - assert.equal(this.$('input').prop('placeholder'), 'placeholder.i18n.key'); + assert.ok(this.$("input[type=text]").length); + assert.equal(this.$("input").prop("placeholder"), "placeholder.i18n.key"); } }); @@ -30,7 +30,7 @@ componentTest("sets the dir attribute to ltr for Hebrew text", { }, test(assert) { - assert.equal(this.$('input').attr('dir'), 'rtl'); + assert.equal(this.$("input").attr("dir"), "rtl"); } }); @@ -41,8 +41,6 @@ componentTest("sets the dir attribute to ltr for English text", { }, test(assert) { - assert.equal(this.$('input').attr('dir'), 'ltr'); + assert.equal(this.$("input").attr("dir"), "ltr"); } }); - - diff --git a/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 b/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 index 3bdd8ec4fc..26915e8066 100644 --- a/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 +++ b/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 @@ -1,44 +1,66 @@ -import componentTest from 'helpers/component-test'; -import Topic from 'discourse/models/topic'; +import componentTest from "helpers/component-test"; +import Topic from "discourse/models/topic"; const buildTopic = function() { return Topic.create({ id: 1234, - title: 'Qunit Test Topic' + title: "Qunit Test Topic" }); }; -moduleForComponent('topic-footer-mobile-dropdown', { +moduleForComponent("topic-footer-mobile-dropdown", { integration: true, beforeEach: function() { - this.set('subject', selectKit()); + this.set("subject", selectKit()); } }); -componentTest('default', { - template: '{{topic-footer-mobile-dropdown topic=topic}}', +componentTest("default", { + template: "{{topic-footer-mobile-dropdown topic=topic}}", beforeEach() { - this.set('topic', buildTopic()); + this.set("topic", buildTopic()); }, test(assert) { - this.get('subject').expand(); + this.get("subject").expand(); andThen(() => { - assert.equal(this.get('subject').header().title(), 'Topic Controls'); - assert.equal(this.get('subject').header().value(), null); - assert.equal(this.get('subject').rowByIndex(0).name(), 'Bookmark'); - assert.equal(this.get('subject').rowByIndex(1).name(), 'Share'); + assert.equal( + this.get("subject") + .header() + .title(), + "Topic Controls" + ); + assert.equal( + this.get("subject") + .header() + .value(), + null + ); + assert.equal( + this.get("subject") + .rowByIndex(0) + .name(), + "Bookmark" + ); + assert.equal( + this.get("subject") + .rowByIndex(1) + .name(), + "Share" + ); assert.notOk( - this.get('subject').selectedRow().exists(), - 'it doesn’t preselect first row' + this.get("subject") + .selectedRow() + .exists(), + "it doesn’t preselect first row" ); }); - this.get('subject').selectRowByValue('share'); + this.get("subject").selectRowByValue("share"); andThen(() => { - assert.equal(this.get('value'), null, 'it resets the value'); + assert.equal(this.get("value"), null, "it resets the value"); }); } }); diff --git a/test/javascripts/components/topic-notifications-button-test.js.es6 b/test/javascripts/components/topic-notifications-button-test.js.es6 index 8dac873939..de27d63f72 100644 --- a/test/javascripts/components/topic-notifications-button-test.js.es6 +++ b/test/javascripts/components/topic-notifications-button-test.js.es6 @@ -1,5 +1,5 @@ -import componentTest from 'helpers/component-test'; -import Topic from 'discourse/models/topic'; +import componentTest from "helpers/component-test"; +import Topic from "discourse/models/topic"; const buildTopic = function(level) { return Topic.create({ @@ -11,10 +11,11 @@ const buildTopic = function(level) { }); }; -moduleForComponent('topic-notifications-button', { integration: true }); +moduleForComponent("topic-notifications-button", { integration: true }); -componentTest('the header has a localized title', { - template: '{{topic-notifications-button notificationLevel=topic.details.notification_level topic=topic}}', +componentTest("the header has a localized title", { + template: + "{{topic-notifications-button notificationLevel=topic.details.notification_level topic=topic}}", beforeEach() { this.set("topic", buildTopic(1)); @@ -22,13 +23,25 @@ componentTest('the header has a localized title', { test(assert) { andThen(() => { - assert.equal(selectKit().header().name(), "Normal", "it has the correct title"); + assert.equal( + selectKit() + .header() + .name(), + "Normal", + "it has the correct title" + ); }); this.set("topic", buildTopic(2)); andThen(() => { - assert.equal(selectKit().header().name(), "Tracking", "it correctly changes the title"); + assert.equal( + selectKit() + .header() + .name(), + "Tracking", + "it correctly changes the title" + ); }); } }); diff --git a/test/javascripts/components/topic-notifications-options-test.js.es6 b/test/javascripts/components/topic-notifications-options-test.js.es6 new file mode 100644 index 0000000000..cf43ae57cc --- /dev/null +++ b/test/javascripts/components/topic-notifications-options-test.js.es6 @@ -0,0 +1,85 @@ +import componentTest from "helpers/component-test"; +import Topic from "discourse/models/topic"; + +const buildTopic = function(archetype) { + return Topic.create({ + id: 4563, + title: "Qunit Test Topic", + details: { + notification_level: 1 + }, + archetype: archetype + }); +}; + +function extractDescs(rows) { + return Array.from( + rows.find(".desc").map(function() { + return this.textContent.trim(); + }) + ); +} + +function getTranslations(type = "") { + return ["watching", "tracking", "regular", "muted"].map(key => { + return I18n.t(`topic.notifications.${key}${type}.description`); + }); +} + +moduleForComponent("topic-notifications-options", { integration: true }); + +componentTest("regular topic notification level descriptions", { + template: + "{{topic-notifications-options value=topic.details.notification_level topic=topic}}", + + test(assert) { + selectKit().expand(); + this.set("topic", buildTopic("regular")); + + andThen(() => { + const uiTexts = extractDescs(selectKit().rows()); + const descriptions = getTranslations(); + + assert.equal( + uiTexts.length, + descriptions.length, + "it has the correct copy" + ); + uiTexts.forEach((text, index) => { + assert.equal( + text.trim(), + descriptions[index].trim(), + "it has the correct copy" + ); + }); + }); + } +}); + +componentTest("PM topic notification level descriptions", { + template: + "{{topic-notifications-options value=topic.details.notification_level topic=topic}}", + + test(assert) { + selectKit().expand(); + this.set("topic", buildTopic("private_message")); + + andThen(() => { + const uiTexts = extractDescs(selectKit().rows()); + const descriptions = getTranslations("_pm"); + + assert.equal( + uiTexts.length, + descriptions.length, + "it has the correct copy" + ); + uiTexts.forEach((text, index) => { + assert.equal( + text.trim(), + descriptions[index].trim(), + "it has the correct copy" + ); + }); + }); + } +}); diff --git a/test/javascripts/components/value-list-test.js.es6 b/test/javascripts/components/value-list-test.js.es6 index feefcbea07..aaa936c901 100644 --- a/test/javascripts/components/value-list-test.js.es6 +++ b/test/javascripts/components/value-list-test.js.es6 @@ -1,67 +1,76 @@ -import componentTest from 'helpers/component-test'; -moduleForComponent('value-list', {integration: true}); +import componentTest from "helpers/component-test"; +moduleForComponent("value-list", { integration: true }); -componentTest('functionality', { +componentTest("functionality", { template: '{{value-list values=values inputType="array"}}', test(assert) { - assert.ok(this.$('.values .value').length === 0, 'it has no values'); - assert.ok(this.$('input').length, 'it renders the input'); - assert.ok(this.$('.btn-primary[disabled]').length, 'it is disabled with no value'); + assert.ok(this.$(".values .value").length === 0, "it has no values"); + assert.ok(this.$("input").length, "it renders the input"); + assert.ok( + this.$(".btn-primary[disabled]").length, + "it is disabled with no value" + ); - fillIn('input', 'eviltrout'); + fillIn("input", "eviltrout"); andThen(() => { - assert.ok(!this.$('.btn-primary[disabled]').length, "it isn't disabled anymore"); + assert.ok( + !this.$(".btn-primary[disabled]").length, + "it isn't disabled anymore" + ); }); - click('.btn-primary'); + click(".btn-primary"); andThen(() => { - assert.equal(this.$('.values .value').length, 1, 'it adds the value'); - assert.equal(this.$('input').val(), '', 'it clears the input'); - assert.ok(this.$('.btn-primary[disabled]').length, "it is disabled again"); - assert.equal(this.get('values'), 'eviltrout', 'it appends the value'); + assert.equal(this.$(".values .value").length, 1, "it adds the value"); + assert.equal(this.$("input").val(), "", "it clears the input"); + assert.ok( + this.$(".btn-primary[disabled]").length, + "it is disabled again" + ); + assert.equal(this.get("values"), "eviltrout", "it appends the value"); }); - click('.value .btn-small'); + click(".value .btn-small"); andThen(() => { - assert.ok(this.$('.values .value').length === 0, 'it removes the value'); + assert.ok(this.$(".values .value").length === 0, "it removes the value"); }); } }); -componentTest('with string delimited values', { - template: '{{value-list values=valueString}}', +componentTest("with string delimited values", { + template: "{{value-list values=valueString}}", beforeEach() { - this.set('valueString', "hello\nworld"); + this.set("valueString", "hello\nworld"); }, test(assert) { - assert.equal(this.$('.values .value').length, 2); + assert.equal(this.$(".values .value").length, 2); - fillIn('input', 'eviltrout'); - click('.btn-primary'); + fillIn("input", "eviltrout"); + click(".btn-primary"); andThen(() => { - assert.equal(this.$('.values .value').length, 3); - assert.equal(this.get('valueString'), "hello\nworld\neviltrout"); + assert.equal(this.$(".values .value").length, 3); + assert.equal(this.get("valueString"), "hello\nworld\neviltrout"); }); } }); -componentTest('with array values', { +componentTest("with array values", { template: '{{value-list values=valueArray inputType="array"}}', beforeEach() { - this.set('valueArray', ['abc', 'def']); + this.set("valueArray", ["abc", "def"]); }, test(assert) { - assert.equal(this.$('.values .value').length, 2); + assert.equal(this.$(".values .value").length, 2); - fillIn('input', 'eviltrout'); - click('.btn-primary'); + fillIn("input", "eviltrout"); + click(".btn-primary"); andThen(() => { - assert.equal(this.$('.values .value').length, 3); - assert.deepEqual(this.get('valueArray'), ['abc', 'def', 'eviltrout']); + assert.equal(this.$(".values .value").length, 3); + assert.deepEqual(this.get("valueArray"), ["abc", "def", "eviltrout"]); }); } }); diff --git a/test/javascripts/controllers/avatar-selector-test.js.es6 b/test/javascripts/controllers/avatar-selector-test.js.es6 index ce26c62163..b5856e886b 100644 --- a/test/javascripts/controllers/avatar-selector-test.js.es6 +++ b/test/javascripts/controllers/avatar-selector-test.js.es6 @@ -1,10 +1,10 @@ -import { mapRoutes } from 'discourse/mapping-router'; +import { mapRoutes } from "discourse/mapping-router"; moduleFor("controller:avatar-selector", "controller:avatar-selector", { beforeEach() { - this.registry.register('router:main', mapRoutes()); + this.registry.register("router:main", mapRoutes()); }, - needs: ['controller:modal'] + needs: ["controller:modal"] }); QUnit.test("avatarTemplate", function(assert) { @@ -12,16 +12,28 @@ QUnit.test("avatarTemplate", function(assert) { avatarSelectorController.setProperties({ selected: "system", - system_avatar_upload_id:1, - gravatar_avatar_upload_id:2, + system_avatar_upload_id: 1, + gravatar_avatar_upload_id: 2, custom_avatar_upload_id: 3 }); - assert.equal(avatarSelectorController.get("selectedUploadId"), 1, "we are using system by default"); + assert.equal( + avatarSelectorController.get("selectedUploadId"), + 1, + "we are using system by default" + ); - avatarSelectorController.set('selected', 'gravatar'); - assert.equal(avatarSelectorController.get("selectedUploadId"), 2, "we are using gravatar when set"); + avatarSelectorController.set("selected", "gravatar"); + assert.equal( + avatarSelectorController.get("selectedUploadId"), + 2, + "we are using gravatar when set" + ); avatarSelectorController.set("selected", "custom"); - assert.equal(avatarSelectorController.get("selectedUploadId"), 3, "we are using custom when set"); + assert.equal( + avatarSelectorController.get("selectedUploadId"), + 3, + "we are using custom when set" + ); }); diff --git a/test/javascripts/controllers/create-account-test.js.es6 b/test/javascripts/controllers/create-account-test.js.es6 index f5604d8169..ec2d425084 100644 --- a/test/javascripts/controllers/create-account-test.js.es6 +++ b/test/javascripts/controllers/create-account-test.js.es6 @@ -1,55 +1,86 @@ -import { mapRoutes } from 'discourse/mapping-router'; +import { mapRoutes } from "discourse/mapping-router"; moduleFor("controller:create-account", "controller:create-account", { beforeEach() { - this.registry.register('router:main', mapRoutes()); + this.registry.register("router:main", mapRoutes()); }, - needs: ['controller:modal', 'controller:login'] + needs: ["controller:modal", "controller:login"] }); -QUnit.test('basicUsernameValidation', function(assert) { +QUnit.test("basicUsernameValidation", function(assert) { var subject = this.subject; var testInvalidUsername = function(username, expectedReason) { var controller = subject({ siteSettings: Discourse.SiteSettings }); - controller.set('accountUsername', username); - assert.equal(controller.get('basicUsernameValidation.failed'), true, 'username should be invalid: ' + username); - assert.equal(controller.get('basicUsernameValidation.reason'), expectedReason, 'username validation reason: ' + username + ', ' + expectedReason); + controller.set("accountUsername", username); + assert.equal( + controller.get("basicUsernameValidation.failed"), + true, + "username should be invalid: " + username + ); + assert.equal( + controller.get("basicUsernameValidation.reason"), + expectedReason, + "username validation reason: " + username + ", " + expectedReason + ); }; - testInvalidUsername('', undefined); - testInvalidUsername('x', I18n.t('user.username.too_short')); - testInvalidUsername('123456789012345678901', I18n.t('user.username.too_long')); + testInvalidUsername("", undefined); + testInvalidUsername("x", I18n.t("user.username.too_short")); + testInvalidUsername( + "123456789012345678901", + I18n.t("user.username.too_long") + ); var controller = subject({ siteSettings: Discourse.SiteSettings }); - controller.set('accountUsername', 'porkchops'); - controller.set('prefilledUsername', 'porkchops'); - assert.equal(controller.get('basicUsernameValidation.ok'), true, 'Prefilled username is valid'); - assert.equal(controller.get('basicUsernameValidation.reason'), I18n.t('user.username.prefilled'), 'Prefilled username is valid'); + controller.set("accountUsername", "porkchops"); + controller.set("prefilledUsername", "porkchops"); + assert.equal( + controller.get("basicUsernameValidation.ok"), + true, + "Prefilled username is valid" + ); + assert.equal( + controller.get("basicUsernameValidation.reason"), + I18n.t("user.username.prefilled"), + "Prefilled username is valid" + ); }); -QUnit.test('passwordValidation', function(assert) { +QUnit.test("passwordValidation", function(assert) { var subject = this.subject; var controller = subject({ siteSettings: Discourse.SiteSettings }); - controller.set('passwordRequired', true); - controller.set('accountEmail', 'pork@chops.com'); - controller.set('accountUsername', 'porkchops'); - controller.set('prefilledUsername', 'porkchops'); + controller.set("passwordRequired", true); + controller.set("accountEmail", "pork@chops.com"); + controller.set("accountUsername", "porkchops"); + controller.set("prefilledUsername", "porkchops"); - controller.set('accountPassword', 'b4fcdae11f9167'); - assert.equal(controller.get('passwordValidation.ok'), true, 'Password is ok'); - assert.equal(controller.get('passwordValidation.reason'), I18n.t('user.password.ok'), 'Password is valid'); + controller.set("accountPassword", "b4fcdae11f9167"); + assert.equal(controller.get("passwordValidation.ok"), true, "Password is ok"); + assert.equal( + controller.get("passwordValidation.reason"), + I18n.t("user.password.ok"), + "Password is valid" + ); var testInvalidPassword = function(password, expectedReason) { var c = subject({ siteSettings: Discourse.SiteSettings }); - c.set('accountPassword', password); - assert.equal(c.get('passwordValidation.failed'), true, 'password should be invalid: ' + password); - assert.equal(c.get('passwordValidation.reason'), expectedReason, 'password validation reason: ' + password + ', ' + expectedReason); + c.set("accountPassword", password); + assert.equal( + c.get("passwordValidation.failed"), + true, + "password should be invalid: " + password + ); + assert.equal( + c.get("passwordValidation.reason"), + expectedReason, + "password validation reason: " + password + ", " + expectedReason + ); }; - testInvalidPassword('', undefined); - testInvalidPassword('x', I18n.t('user.password.too_short')); - testInvalidPassword('porkchops', I18n.t('user.password.same_as_username')); - testInvalidPassword('pork@chops.com', I18n.t('user.password.same_as_email')); + testInvalidPassword("", undefined); + testInvalidPassword("x", I18n.t("user.password.too_short")); + testInvalidPassword("porkchops", I18n.t("user.password.same_as_username")); + testInvalidPassword("pork@chops.com", I18n.t("user.password.same_as_email")); }); diff --git a/test/javascripts/controllers/group-test.js.es6 b/test/javascripts/controllers/group-test.js.es6 index a5defd2d14..eaf09be5be 100644 --- a/test/javascripts/controllers/group-test.js.es6 +++ b/test/javascripts/controllers/group-test.js.es6 @@ -1,5 +1,5 @@ moduleFor("controller:group", { - needs: ['controller:application'] + needs: ["controller:application"] }); QUnit.test("canEditGroup", function(assert) { @@ -9,13 +9,25 @@ QUnit.test("canEditGroup", function(assert) { model: { is_group_owner: true, automatic: true } }); - assert.equal(GroupController.get("canEditGroup"), false, "automatic groups cannot be edited"); + assert.equal( + GroupController.get("canEditGroup"), + false, + "automatic groups cannot be edited" + ); GroupController.set("model.automatic", false); - assert.equal(GroupController.get("canEditGroup"), true, "owners can edit groups"); + assert.equal( + GroupController.get("canEditGroup"), + true, + "owners can edit groups" + ); GroupController.set("model.is_group_owner", false); - assert.equal(GroupController.get("canEditGroup"), false, "normal users cannot edit groups"); + assert.equal( + GroupController.get("canEditGroup"), + false, + "normal users cannot edit groups" + ); }); diff --git a/test/javascripts/controllers/history-test.js.es6 b/test/javascripts/controllers/history-test.js.es6 index 11fe31a27e..c2eca9962a 100644 --- a/test/javascripts/controllers/history-test.js.es6 +++ b/test/javascripts/controllers/history-test.js.es6 @@ -8,21 +8,24 @@ QUnit.test("displayEdit", function(assert) { }); assert.equal( - HistoryController.get("displayEdit"), false, + HistoryController.get("displayEdit"), + false, "it should not display edit button when user cannot edit the post" ); HistoryController.set("model.can_edit", true); assert.equal( - HistoryController.get("displayEdit"), true, + HistoryController.get("displayEdit"), + true, "it should display edit button when user can edit the post" ); HistoryController.set("model.current_revision", 2); assert.equal( - HistoryController.get("displayEdit"), false, + HistoryController.get("displayEdit"), + false, "it should only display the edit button on the latest revision" ); }); diff --git a/test/javascripts/controllers/preferences-second-factor-test.js.es6 b/test/javascripts/controllers/preferences-second-factor-test.js.es6 index 0bbf07100a..753552674f 100644 --- a/test/javascripts/controllers/preferences-second-factor-test.js.es6 +++ b/test/javascripts/controllers/preferences-second-factor-test.js.es6 @@ -1,27 +1,26 @@ moduleFor("controller:preferences/second-factor"); -QUnit.test("displayOAuthWarning when OAuth login methods are disabled", function(assert) { - const controller = this.subject({ - siteSettings: { - enable_google_oauth2_logins: false - } - }); +QUnit.test( + "displayOAuthWarning when OAuth login methods are disabled", + function(assert) { + const controller = this.subject({ + siteSettings: { + enable_google_oauth2_logins: false + } + }); - assert.equal( - controller.get('displayOAuthWarning'), - false - ); -}); + assert.equal(controller.get("displayOAuthWarning"), false); + } +); -QUnit.test("displayOAuthWarning when OAuth login methods are enabled", function(assert) { +QUnit.test("displayOAuthWarning when OAuth login methods are enabled", function( + assert +) { const controller = this.subject({ siteSettings: { enable_google_oauth2_logins: true } }); - assert.equal( - controller.get('displayOAuthWarning'), - true - ); + assert.equal(controller.get("displayOAuthWarning"), true); }); diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6 index eeb72861e9..d8a907acad 100644 --- a/test/javascripts/controllers/topic-test.js.es6 +++ b/test/javascripts/controllers/topic-test.js.es6 @@ -4,7 +4,9 @@ import Topic from "discourse/models/topic"; moduleFor("controller:topic", "controller:topic", { needs: ["controller:composer", "controller:application"], beforeEach() { - this.registry.register("app-events:main", AppEvents.create(), { instantiate: false }); + this.registry.register("app-events:main", AppEvents.create(), { + instantiate: false + }); this.registry.injection("controller", "appEvents", "app-events:main"); } }); @@ -18,41 +20,70 @@ QUnit.test("editTopic", function(assert) { controller.set("model.details.can_edit", false); controller.send("editTopic"); - assert.not(controller.get("editingTopic"), "calling editTopic doesn't enable editing unless the user can edit"); + assert.not( + controller.get("editingTopic"), + "calling editTopic doesn't enable editing unless the user can edit" + ); controller.set("model.details.can_edit", true); controller.send("editTopic"); - assert.ok(controller.get("editingTopic"), "calling editTopic enables editing if the user can edit"); + assert.ok( + controller.get("editingTopic"), + "calling editTopic enables editing if the user can edit" + ); assert.equal(controller.get("buffered.title"), model.get("title")); - assert.equal(controller.get("buffered.category_id"), model.get("category_id")); + assert.equal( + controller.get("buffered.category_id"), + model.get("category_id") + ); controller.send("cancelEditingTopic"); - assert.not(controller.get("editingTopic"), "cancelling edit mode reverts the property value"); + assert.not( + controller.get("editingTopic"), + "cancelling edit mode reverts the property value" + ); }); QUnit.test("toggleMultiSelect", function(assert) { const model = Topic.create(); const controller = this.subject({ model }); - assert.not(controller.get("multiSelect"), "multi selection mode is disabled by default"); + assert.not( + controller.get("multiSelect"), + "multi selection mode is disabled by default" + ); controller.get("selectedPostIds").pushObject(1); assert.equal(controller.get("selectedPostIds.length"), 1); controller.send("toggleMultiSelect"); - assert.ok(controller.get("multiSelect"), "calling 'toggleMultiSelect' once enables multi selection mode"); - assert.equal(controller.get("selectedPostIds.length"), 0, "toggling 'multiSelect' clears 'selectedPostIds'"); + assert.ok( + controller.get("multiSelect"), + "calling 'toggleMultiSelect' once enables multi selection mode" + ); + assert.equal( + controller.get("selectedPostIds.length"), + 0, + "toggling 'multiSelect' clears 'selectedPostIds'" + ); controller.get("selectedPostIds").pushObject(2); assert.equal(controller.get("selectedPostIds.length"), 1); controller.send("toggleMultiSelect"); - assert.not(controller.get("multiSelect"), "calling 'toggleMultiSelect' twice disables multi selection mode"); - assert.equal(controller.get("selectedPostIds.length"), 0, "toggling 'multiSelect' clears 'selectedPostIds'"); + assert.not( + controller.get("multiSelect"), + "calling 'toggleMultiSelect' twice disables multi selection mode" + ); + assert.equal( + controller.get("selectedPostIds.length"), + 0, + "toggling 'multiSelect' clears 'selectedPostIds'" + ); }); QUnit.test("selectedPosts", function(assert) { @@ -62,8 +93,15 @@ QUnit.test("selectedPosts", function(assert) { controller.set("selectedPostIds", [1, 2, 42]); - assert.equal(controller.get("selectedPosts.length"), 2, "selectedPosts only contains already loaded posts"); - assert.not(controller.get("selectedPosts").some(p => p === undefined), "selectedPosts only contains valid post objects"); + assert.equal( + controller.get("selectedPosts.length"), + 2, + "selectedPosts only contains already loaded posts" + ); + assert.not( + controller.get("selectedPosts").some(p => p === undefined), + "selectedPosts only contains valid post objects" + ); }); QUnit.test("selectedAllPosts", function(assert) { @@ -81,7 +119,10 @@ QUnit.test("selectedAllPosts", function(assert) { controller.get("selectedPostIds").pushObject(42); - assert.ok(controller.get("selectedAllPosts"), "all posts (including filtered posts) are selected"); + assert.ok( + controller.get("selectedAllPosts"), + "all posts (including filtered posts) are selected" + ); }); QUnit.test("selectedPostsUsername", function(assert) { @@ -89,7 +130,7 @@ QUnit.test("selectedPostsUsername", function(assert) { posts: [ { id: 1, username: "gary" }, { id: 2, username: "gary" }, - { id: 3, username: "lili" }, + { id: 3, username: "lili" } ], stream: [1, 2, 3] }; @@ -98,23 +139,43 @@ QUnit.test("selectedPostsUsername", function(assert) { const controller = this.subject({ model }); const selectedPostIds = controller.get("selectedPostIds"); - assert.equal(controller.get("selectedPostsUsername"), undefined, "no username when no selected posts"); + assert.equal( + controller.get("selectedPostsUsername"), + undefined, + "no username when no selected posts" + ); selectedPostIds.pushObject(1); - assert.equal(controller.get("selectedPostsUsername"), "gary", "username of the selected posts"); + assert.equal( + controller.get("selectedPostsUsername"), + "gary", + "username of the selected posts" + ); selectedPostIds.pushObject(2); - assert.equal(controller.get("selectedPostsUsername"), "gary", "username of all the selected posts when same user"); + assert.equal( + controller.get("selectedPostsUsername"), + "gary", + "username of all the selected posts when same user" + ); selectedPostIds.pushObject(3); - assert.equal(controller.get("selectedPostsUsername"), undefined, "no username when more than 1 user"); + assert.equal( + controller.get("selectedPostsUsername"), + undefined, + "no username when more than 1 user" + ); selectedPostIds.replace(2, 1, [42]); - assert.equal(controller.get("selectedPostsUsername"), undefined, "no username when not already loaded posts are selected"); + assert.equal( + controller.get("selectedPostsUsername"), + undefined, + "no username when not already loaded posts are selected" + ); }); QUnit.test("showSelectedPostsAtBottom", function(assert) { @@ -126,11 +187,17 @@ QUnit.test("showSelectedPostsAtBottom", function(assert) { site.set("mobileView", true); - assert.not(controller.get("showSelectedPostsAtBottom"), "requires at least 3 posts on mobile"); + assert.not( + controller.get("showSelectedPostsAtBottom"), + "requires at least 3 posts on mobile" + ); model.set("posts_count", 4); - assert.ok(controller.get("showSelectedPostsAtBottom"), "true when mobile and more than 3 posts"); + assert.ok( + controller.get("showSelectedPostsAtBottom"), + "true when mobile and more than 3 posts" + ); }); QUnit.test("canDeleteSelected", function(assert) { @@ -147,19 +214,31 @@ QUnit.test("canDeleteSelected", function(assert) { const controller = this.subject({ model }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not(controller.get("canDeleteSelected"), "false when no posts are selected"); + assert.not( + controller.get("canDeleteSelected"), + "false when no posts are selected" + ); selectedPostIds.pushObject(1); - assert.not(controller.get("canDeleteSelected"), "false when can't delete one of the selected posts"); + assert.not( + controller.get("canDeleteSelected"), + "false when can't delete one of the selected posts" + ); selectedPostIds.replace(0, 1, [2, 3]); - assert.ok(controller.get("canDeleteSelected"), "true when all selected posts can be deleted"); + assert.ok( + controller.get("canDeleteSelected"), + "true when all selected posts can be deleted" + ); selectedPostIds.pushObject(1); - assert.ok(controller.get("canDeleteSelected"), "true when all posts are selected"); + assert.ok( + controller.get("canDeleteSelected"), + "true when all posts are selected" + ); }); QUnit.test("Can split/merge topic", function(assert) { @@ -172,17 +251,32 @@ QUnit.test("Can split/merge topic", function(assert) { stream: [1, 2, 3] }; - const model = Topic.create({ postStream, details: { can_move_posts: false } }); + const model = Topic.create({ + postStream, + details: { can_move_posts: false } + }); const controller = this.subject({ model }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not(controller.get("canSplitTopic"), "can't split topic when no posts are selected"); - assert.not(controller.get("canMergeTopic"), "can't merge topic when no posts are selected"); + assert.not( + controller.get("canSplitTopic"), + "can't split topic when no posts are selected" + ); + assert.not( + controller.get("canMergeTopic"), + "can't merge topic when no posts are selected" + ); selectedPostIds.pushObject(1); - assert.not(controller.get("canSplitTopic"), "can't split topic when can't move posts"); - assert.not(controller.get("canMergeTopic"), "can't merge topic when can't move posts"); + assert.not( + controller.get("canSplitTopic"), + "can't split topic when can't move posts" + ); + assert.not( + controller.get("canMergeTopic"), + "can't merge topic when can't move posts" + ); model.set("details.can_move_posts", true); @@ -192,33 +286,47 @@ QUnit.test("Can split/merge topic", function(assert) { selectedPostIds.removeObject(1); selectedPostIds.pushObject(2); - assert.not(controller.get("canSplitTopic"), "can't split topic when 1st post is not a regular post"); - assert.ok(controller.get("canMergeTopic"), "can merge topic when 1st post is not a regular post"); + assert.not( + controller.get("canSplitTopic"), + "can't split topic when 1st post is not a regular post" + ); + assert.ok( + controller.get("canMergeTopic"), + "can merge topic when 1st post is not a regular post" + ); selectedPostIds.pushObject(3); - assert.not(controller.get("canSplitTopic"), "can't split topic when all posts are selected"); - assert.ok(controller.get("canMergeTopic"), "can merge topic when all posts are selected"); + assert.not( + controller.get("canSplitTopic"), + "can't split topic when all posts are selected" + ); + assert.ok( + controller.get("canMergeTopic"), + "can merge topic when all posts are selected" + ); }); QUnit.test("canChangeOwner", function(assert) { const currentUser = Discourse.User.create({ admin: false }); - this.registry.register("current-user:main", currentUser, { instantiate: false }); + this.registry.register("current-user:main", currentUser, { + instantiate: false + }); this.registry.injection("controller", "currentUser", "current-user:main"); const postStream = { - posts: [ - { id: 1, username: "gary" }, - { id: 2, username: "lili" }, - ], + posts: [{ id: 1, username: "gary" }, { id: 2, username: "lili" }], stream: [1, 2] }; - const model = Topic.create({ postStream, currentUser: { admin: false }}); + const model = Topic.create({ postStream, currentUser: { admin: false } }); const controller = this.subject({ model }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not(controller.get("canChangeOwner"), "false when no posts are selected"); + assert.not( + controller.get("canChangeOwner"), + "false when no posts are selected" + ); selectedPostIds.pushObject(1); @@ -226,11 +334,17 @@ QUnit.test("canChangeOwner", function(assert) { currentUser.set("admin", true); - assert.ok(controller.get("canChangeOwner"), "true when admin and one post is selected"); + assert.ok( + controller.get("canChangeOwner"), + "true when admin and one post is selected" + ); selectedPostIds.pushObject(2); - assert.not(controller.get("canChangeOwner"), "false when admin but more than 1 user"); + assert.not( + controller.get("canChangeOwner"), + "false when admin but more than 1 user" + ); }); QUnit.test("canMergePosts", function(assert) { @@ -239,7 +353,7 @@ QUnit.test("canMergePosts", function(assert) { { id: 1, username: "gary", can_delete: true }, { id: 2, username: "lili", can_delete: true }, { id: 3, username: "gary", can_delete: false }, - { id: 4, username: "gary", can_delete: true }, + { id: 4, username: "gary", can_delete: true } ], stream: [1, 2, 3] }; @@ -248,23 +362,38 @@ QUnit.test("canMergePosts", function(assert) { const controller = this.subject({ model }); const selectedPostIds = controller.get("selectedPostIds"); - assert.not(controller.get("canMergePosts"), "false when no posts are selected"); + assert.not( + controller.get("canMergePosts"), + "false when no posts are selected" + ); selectedPostIds.pushObject(1); - assert.not(controller.get("canMergePosts"), "false when only one post is selected"); + assert.not( + controller.get("canMergePosts"), + "false when only one post is selected" + ); selectedPostIds.pushObject(2); - assert.not(controller.get("canMergePosts"), "false when selected posts are from different users"); + assert.not( + controller.get("canMergePosts"), + "false when selected posts are from different users" + ); selectedPostIds.replace(1, 1, [3]); - assert.not(controller.get("canMergePosts"), "false when selected posts can't be deleted"); + assert.not( + controller.get("canMergePosts"), + "false when selected posts can't be deleted" + ); selectedPostIds.replace(1, 1, [4]); - assert.ok(controller.get("canMergePosts"), "true when all selected posts are deletable and by the same user"); + assert.ok( + controller.get("canMergePosts"), + "true when all selected posts are deletable and by the same user" + ); }); QUnit.test("Select/deselect all", function(assert) { @@ -272,15 +401,27 @@ QUnit.test("Select/deselect all", function(assert) { const model = Topic.create({ postStream }); const controller = this.subject({ model }); - assert.equal(controller.get("selectedPostsCount"), 0, "no posts selected by default"); + assert.equal( + controller.get("selectedPostsCount"), + 0, + "no posts selected by default" + ); controller.send("selectAll"); - assert.equal(controller.get("selectedPostsCount"), postStream.stream.length, "calling 'selectAll' selects all posts"); + assert.equal( + controller.get("selectedPostsCount"), + postStream.stream.length, + "calling 'selectAll' selects all posts" + ); controller.send("deselectAll"); - assert.equal(controller.get("selectedPostsCount"), 0, "calling 'deselectAll' deselects all posts"); + assert.equal( + controller.get("selectedPostsCount"), + 0, + "calling 'deselectAll' deselects all posts" + ); }); QUnit.test("togglePostSelection", function(assert) { @@ -291,11 +432,19 @@ QUnit.test("togglePostSelection", function(assert) { controller.send("togglePostSelection", { id: 1 }); - assert.equal(selectedPostIds[0], 1, "adds the selected post id if not already selected"); + assert.equal( + selectedPostIds[0], + 1, + "adds the selected post id if not already selected" + ); controller.send("togglePostSelection", { id: 1 }); - assert.equal(selectedPostIds[0], undefined, "removes the selected post id if already selected"); + assert.equal( + selectedPostIds[0], + undefined, + "removes the selected post id if already selected" + ); }); // QUnit.test("selectReplies", function(assert) { diff --git a/test/javascripts/ember/resolver-test.js.es6 b/test/javascripts/ember/resolver-test.js.es6 index 85424065ec..e374abc56f 100644 --- a/test/javascripts/ember/resolver-test.js.es6 +++ b/test/javascripts/ember/resolver-test.js.es6 @@ -1,4 +1,4 @@ -import { setResolverOption, buildResolver } from 'discourse-common/resolver'; +import { setResolverOption, buildResolver } from "discourse-common/resolver"; let originalTemplates; let resolver; @@ -15,7 +15,7 @@ function setTemplates(lookupTemplateStrings) { }); } -const DiscourseResolver = buildResolver('discourse'); +const DiscourseResolver = buildResolver("discourse"); QUnit.module("lib:resolver", { beforeEach() { @@ -31,12 +31,7 @@ QUnit.module("lib:resolver", { }); QUnit.test("finds templates in top level dir", assert => { - setTemplates([ - "foobar", - "fooBar", - "foo_bar", - "foo.bar" - ]); + setTemplates(["foobar", "fooBar", "foo_bar", "foo.bar"]); lookupTemplate(assert, "template:foobar", "foobar", "by lowcased name"); lookupTemplate(assert, "template:fooBar", "fooBar", "by camel cased name"); @@ -45,96 +40,224 @@ QUnit.test("finds templates in top level dir", assert => { }); QUnit.test("finds templates in first-level subdir", assert => { - setTemplates([ - "foo/bar_baz" - ]); + setTemplates(["foo/bar_baz"]); - lookupTemplate(assert, "template:foo/bar_baz", "foo/bar_baz", "with subdir defined by slash"); - lookupTemplate(assert, "template:foo.bar_baz", "foo/bar_baz", "with subdir defined by dot"); - lookupTemplate(assert, "template:fooBarBaz", "foo/bar_baz", "with subdir defined by first camel case and the rest of camel cases converted to underscores"); - lookupTemplate(assert, "template:foo_bar_baz", "foo/bar_baz", "with subdir defined by first underscore"); + lookupTemplate( + assert, + "template:foo/bar_baz", + "foo/bar_baz", + "with subdir defined by slash" + ); + lookupTemplate( + assert, + "template:foo.bar_baz", + "foo/bar_baz", + "with subdir defined by dot" + ); + lookupTemplate( + assert, + "template:fooBarBaz", + "foo/bar_baz", + "with subdir defined by first camel case and the rest of camel cases converted to underscores" + ); + lookupTemplate( + assert, + "template:foo_bar_baz", + "foo/bar_baz", + "with subdir defined by first underscore" + ); }); -QUnit.test("resolves precedence between overlapping top level dir and first level subdir templates", assert => { - setTemplates([ - "fooBar", - "foo_bar", - "foo.bar", - "foo/bar" - ]); +QUnit.test( + "resolves precedence between overlapping top level dir and first level subdir templates", + assert => { + setTemplates(["fooBar", "foo_bar", "foo.bar", "foo/bar"]); - lookupTemplate(assert, "template:foo.bar", "foo/bar", "preferring first level subdir for dotted name"); - lookupTemplate(assert, "template:fooBar", "fooBar", "preferring top level dir for camel cased name"); - lookupTemplate(assert, "template:foo_bar", "foo_bar", "preferring top level dir for underscored name"); -}); + lookupTemplate( + assert, + "template:foo.bar", + "foo/bar", + "preferring first level subdir for dotted name" + ); + lookupTemplate( + assert, + "template:fooBar", + "fooBar", + "preferring top level dir for camel cased name" + ); + lookupTemplate( + assert, + "template:foo_bar", + "foo_bar", + "preferring top level dir for underscored name" + ); + } +); QUnit.test("finds templates in subdir deeper than one level", assert => { - setTemplates([ - "foo/bar/baz/qux" - ]); + setTemplates(["foo/bar/baz/qux"]); - lookupTemplate(assert, "template:foo/bar/baz/qux", "foo/bar/baz/qux", "for subdirs defined by slashes"); - lookupTemplate(assert, "template:foo.bar.baz.qux", "foo/bar/baz/qux", "for subdirs defined by dots"); - lookupTemplate(assert, "template:foo/bar/bazQux", "foo/bar/baz/qux", "for subdirs defined by slashes plus one camel case"); - lookupTemplate(assert, "template:foo/bar/baz_qux", "foo/bar/baz/qux", "for subdirs defined by slashes plus one underscore"); + lookupTemplate( + assert, + "template:foo/bar/baz/qux", + "foo/bar/baz/qux", + "for subdirs defined by slashes" + ); + lookupTemplate( + assert, + "template:foo.bar.baz.qux", + "foo/bar/baz/qux", + "for subdirs defined by dots" + ); + lookupTemplate( + assert, + "template:foo/bar/bazQux", + "foo/bar/baz/qux", + "for subdirs defined by slashes plus one camel case" + ); + lookupTemplate( + assert, + "template:foo/bar/baz_qux", + "foo/bar/baz/qux", + "for subdirs defined by slashes plus one underscore" + ); - lookupTemplate(assert, "template:fooBarBazQux", undefined, "but not for subdirs defined by more than one camel case"); - lookupTemplate(assert, "template:foo_bar_baz_qux", undefined, "but not for subdirs defined by more than one underscore"); - lookupTemplate(assert, "template:foo.bar.bazQux", undefined, "but not for subdirs defined by dots plus one camel case"); - lookupTemplate(assert, "template:foo.bar.baz_qux", undefined, "but not for subdirs defined by dots plus one underscore"); + lookupTemplate( + assert, + "template:fooBarBazQux", + undefined, + "but not for subdirs defined by more than one camel case" + ); + lookupTemplate( + assert, + "template:foo_bar_baz_qux", + undefined, + "but not for subdirs defined by more than one underscore" + ); + lookupTemplate( + assert, + "template:foo.bar.bazQux", + undefined, + "but not for subdirs defined by dots plus one camel case" + ); + lookupTemplate( + assert, + "template:foo.bar.baz_qux", + undefined, + "but not for subdirs defined by dots plus one underscore" + ); }); QUnit.test("resolves mobile templates to 'mobile/' namespace", assert => { - setTemplates([ + setTemplates(["mobile/foo", "bar", "mobile/bar", "baz"]); + + setResolverOption("mobileView", true); + + lookupTemplate( + assert, + "template:foo", "mobile/foo", - "bar", + "finding mobile version even if normal one is not present" + ); + lookupTemplate( + assert, + "template:bar", "mobile/bar", - "baz" - ]); - - setResolverOption('mobileView', true); - - lookupTemplate(assert, "template:foo", "mobile/foo", "finding mobile version even if normal one is not present"); - lookupTemplate(assert, "template:bar", "mobile/bar", "preferring mobile version when both mobile and normal versions are present"); - lookupTemplate(assert, "template:baz", "baz", "falling back to a normal version when mobile version is not present"); + "preferring mobile version when both mobile and normal versions are present" + ); + lookupTemplate( + assert, + "template:baz", + "baz", + "falling back to a normal version when mobile version is not present" + ); }); QUnit.test("resolves plugin templates to 'javascripts/' namespace", assert => { - setTemplates([ + setTemplates(["javascripts/foo", "bar", "javascripts/bar", "baz"]); + + lookupTemplate( + assert, + "template:foo", "javascripts/foo", - "bar", + "finding plugin version even if normal one is not present" + ); + lookupTemplate( + assert, + "template:bar", "javascripts/bar", - "baz" - ]); - - lookupTemplate(assert, "template:foo", "javascripts/foo", "finding plugin version even if normal one is not present"); - lookupTemplate(assert, "template:bar", "javascripts/bar", "preferring plugin version when both versions are present"); - lookupTemplate(assert, "template:baz", "baz", "falling back to a normal version when plugin version is not present"); + "preferring plugin version when both versions are present" + ); + lookupTemplate( + assert, + "template:baz", + "baz", + "falling back to a normal version when plugin version is not present" + ); }); -QUnit.test("resolves templates with 'admin' prefix to 'admin/templates/' namespace", assert => { - setTemplates([ - "admin/templates/foo", - "adminBar", - "admin_bar", - "admin.bar", - "admin/templates/bar" - ]); +QUnit.test( + "resolves templates with 'admin' prefix to 'admin/templates/' namespace", + assert => { + setTemplates([ + "admin/templates/foo", + "adminBar", + "admin_bar", + "admin.bar", + "admin/templates/bar" + ]); - lookupTemplate(assert, "template:adminFoo", "admin/templates/foo", "when prefix is separated by camel case"); - lookupTemplate(assert, "template:admin_foo", "admin/templates/foo", "when prefix is separated by underscore"); - lookupTemplate(assert, "template:admin.foo", "admin/templates/foo", "when prefix is separated by dot"); + lookupTemplate( + assert, + "template:adminFoo", + "admin/templates/foo", + "when prefix is separated by camel case" + ); + lookupTemplate( + assert, + "template:admin_foo", + "admin/templates/foo", + "when prefix is separated by underscore" + ); + lookupTemplate( + assert, + "template:admin.foo", + "admin/templates/foo", + "when prefix is separated by dot" + ); - lookupTemplate(assert, "template:adminfoo", undefined, "but not when prefix is not separated in any way"); - lookupTemplate(assert, "template:adminBar", "adminBar", "but not when template with the exact camel cased name exists"); - lookupTemplate(assert, "template:admin_bar", "admin_bar", "but not when template with the exact underscored name exists"); - lookupTemplate(assert, "template:admin.bar", "admin.bar", "but not when template with the exact dotted name exists"); -}); + lookupTemplate( + assert, + "template:adminfoo", + undefined, + "but not when prefix is not separated in any way" + ); + lookupTemplate( + assert, + "template:adminBar", + "adminBar", + "but not when template with the exact camel cased name exists" + ); + lookupTemplate( + assert, + "template:admin_bar", + "admin_bar", + "but not when template with the exact underscored name exists" + ); + lookupTemplate( + assert, + "template:admin.bar", + "admin.bar", + "but not when template with the exact dotted name exists" + ); + } +); -QUnit.test("returns 'not_found' template when template name cannot be resolved", assert => { - setTemplates([ - "not_found" - ]); +QUnit.test( + "returns 'not_found' template when template name cannot be resolved", + assert => { + setTemplates(["not_found"]); - lookupTemplate(assert, "template:foo/bar/baz", "not_found", ""); -}); + lookupTemplate(assert, "template:foo/bar/baz", "not_found", ""); + } +); diff --git a/test/javascripts/fixtures/about.js.es6 b/test/javascripts/fixtures/about.js.es6 index 4261dd2a48..08f89c2f1f 100644 --- a/test/javascripts/fixtures/about.js.es6 +++ b/test/javascripts/fixtures/about.js.es6 @@ -1,3 +1,109 @@ export default { - "about.json": {"about":{"stats":{"topic_count":5969,"post_count":65860,"user_count":10858,"topics_7_days":112,"posts_7_days":1302,"users_7_days":111,"like_count":37747,"likes_7_days":1143},"description":"Discussion about the next-generation open source Discourse forum software","title":"Discourse Meta","locale":"en","version":"0.9.9.16","moderators":[{"id":3,"username":"supermathie","uploaded_avatar_id":5247,"avatar_template":"/images/avatar.png"},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/images/avatar.png"},{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/images/avatar.png"},{"id":2,"username":"neil","uploaded_avatar_id":5245,"avatar_template":"/images/avatar.png"},{"id":1,"username":"sam","uploaded_avatar_id":5243,"avatar_template":"/images/avatar.png"},{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/images/avatar.png"}],"admins":[{"id":3,"username":"supermathie","uploaded_avatar_id":5247,"avatar_template":"/images/avatar.png"},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/images/avatar.png"},{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/images/avatar.png"},{"id":38,"username":"frandallfarmer","uploaded_avatar_id":5307,"avatar_template":"/images/avatar.png"},{"id":6626,"username":"riking","uploaded_avatar_id":9779,"avatar_template":"/images/avatar.png"},{"id":2,"username":"neil","uploaded_avatar_id":5245,"avatar_template":"/images/avatar.png"},{"id":1,"username":"sam","uploaded_avatar_id":5243,"avatar_template":"/images/avatar.png"},{"id":1995,"username":"zogstrip","uploaded_avatar_id":8630,"avatar_template":"/images/avatar.png"}]}} + "about.json": { + about: { + stats: { + topic_count: 5969, + post_count: 65860, + user_count: 10858, + topics_7_days: 112, + posts_7_days: 1302, + users_7_days: 111, + like_count: 37747, + likes_7_days: 1143 + }, + description: + "Discussion about the next-generation open source Discourse forum software", + title: "Discourse Meta", + locale: "en", + version: "0.9.9.16", + moderators: [ + { + id: 3, + username: "supermathie", + uploaded_avatar_id: 5247, + avatar_template: "/images/avatar.png" + }, + { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: "/images/avatar.png" + }, + { + id: 19, + username: "eviltrout", + uploaded_avatar_id: 5275, + avatar_template: "/images/avatar.png" + }, + { + id: 2, + username: "neil", + uploaded_avatar_id: 5245, + avatar_template: "/images/avatar.png" + }, + { + id: 1, + username: "sam", + uploaded_avatar_id: 5243, + avatar_template: "/images/avatar.png" + }, + { + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: "/images/avatar.png" + } + ], + admins: [ + { + id: 3, + username: "supermathie", + uploaded_avatar_id: 5247, + avatar_template: "/images/avatar.png" + }, + { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: "/images/avatar.png" + }, + { + id: 19, + username: "eviltrout", + uploaded_avatar_id: 5275, + avatar_template: "/images/avatar.png" + }, + { + id: 38, + username: "frandallfarmer", + uploaded_avatar_id: 5307, + avatar_template: "/images/avatar.png" + }, + { + id: 6626, + username: "riking", + uploaded_avatar_id: 9779, + avatar_template: "/images/avatar.png" + }, + { + id: 2, + username: "neil", + uploaded_avatar_id: 5245, + avatar_template: "/images/avatar.png" + }, + { + id: 1, + username: "sam", + uploaded_avatar_id: 5243, + avatar_template: "/images/avatar.png" + }, + { + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: "/images/avatar.png" + } + ] + } + } }; diff --git a/test/javascripts/fixtures/badges_fixture.js.es6 b/test/javascripts/fixtures/badges_fixture.js.es6 index 9dcab162e7..43ad075bc7 100644 --- a/test/javascripts/fixtures/badges_fixture.js.es6 +++ b/test/javascripts/fixtures/badges_fixture.js.es6 @@ -1,1679 +1,1682 @@ export default { - "\/badges.json": { - "badge_types": [ + "/badges.json": { + badge_types: [ { - "id": 2, - "name": "Silver" + id: 2, + name: "Silver" }, { - "id": 3, - "name": "Bronze" + id: 3, + name: "Bronze" }, { - "id": 1, - "name": "Gold" + id: 1, + name: "Gold" } ], - "badge_groupings": [ + badge_groupings: [ { - "id": 8, - "name": "Development", - "description": null, - "position": 5 + id: 8, + name: "Development", + description: null, + position: 5 }, { - "id": 4, - "name": "Trust Level", - "description": null, - "position": 3 + id: 4, + name: "Trust Level", + description: null, + position: 3 }, { - "id": 7, - "name": "Testing", - "description": null, - "position": 4 + id: 7, + name: "Testing", + description: null, + position: 4 }, { - "id": 1, - "name": "Getting Started", - "description": null, - "position": 0 + id: 1, + name: "Getting Started", + description: null, + position: 0 }, { - "id": 3, - "name": "Posting", - "description": null, - "position": 2 + id: 3, + name: "Posting", + description: null, + position: 2 }, { - "id": 2, - "name": "Community", - "description": null, - "position": 1 + id: 2, + name: "Community", + description: null, + position: 1 } ], - "badges": [ + badges: [ { - "id": 108, - "name": "Great contributor", - "description": "contributed 25 accepted pull request", - "grant_count": 11, - "allow_title": true, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 8, - "system": false, - "badge_type_id": 2 + id: 108, + name: "Great contributor", + description: "contributed 25 accepted pull request", + grant_count: 11, + allow_title: true, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 8, + system: false, + badge_type_id: 2 }, { - "id": 118, - "name": "Plugin Author", - "description": "Developed a plugin<\/a> for Discourse ", - "grant_count": 10, - "allow_title": true, - "multiple_grant": false, - "icon": "fa-cog", - "listable": true, - "enabled": true, - "badge_grouping_id": 8, - "system": false, - "badge_type_id": 2 + id: 118, + name: "Plugin Author", + description: + 'Developed a plugin for Discourse ', + grant_count: 10, + allow_title: true, + multiple_grant: false, + icon: "fa-cog", + listable: true, + enabled: true, + badge_grouping_id: 8, + system: false, + badge_type_id: 2 }, { - "id": 3, - "name": "Leader", - "description": null, - "grant_count": 29, - "allow_title": true, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 4, - "system": true, - "badge_type_id": 2 + id: 3, + name: "Leader", + description: null, + grant_count: 29, + allow_title: true, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + badge_type_id: 2 }, { - "id": 107, - "name": "Contributor", - "description": "contributed an accepted pull request", - "grant_count": 200, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 8, - "system": false, - "badge_type_id": 3 + id: 107, + name: "Contributor", + description: "contributed an accepted pull request", + grant_count: 200, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 8, + system: false, + badge_type_id: 3 }, { - "id": 116, - "name": "Tester", - "description": "Reported 10 bugs that were liked by the Discourse team<\/a>", - "grant_count": 9, - "allow_title": true, - "multiple_grant": false, - "icon": "fa-bug", - "listable": true, - "enabled": true, - "badge_grouping_id": 7, - "system": false, - "badge_type_id": 2 + id: 116, + name: "Tester", + description: + 'Reported 10 bugs that were liked by the Discourse team', + grant_count: 9, + allow_title: true, + multiple_grant: false, + icon: "fa-bug", + listable: true, + enabled: true, + badge_grouping_id: 7, + system: false, + badge_type_id: 2 }, { - "id": 109, - "name": "Amazing contributor", - "description": "contributed 250 accepted pull request", - "grant_count": 0, - "allow_title": true, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 8, - "system": false, - "badge_type_id": 1 + id: 109, + name: "Amazing contributor", + description: "contributed 250 accepted pull request", + grant_count: 0, + allow_title: true, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 8, + system: false, + badge_type_id: 1 }, { - "id": 2, - "name": "Regular User", - "description": null, - "grant_count": 467, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 4, - "system": true, - "badge_type_id": 3 + id: 2, + name: "Regular User", + description: null, + grant_count: 467, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + badge_type_id: 3 }, { - "id": 114, - "name": "Bug Reporter", - "description": "Reported a bug that was liked by the Discourse team<\/a>", - "grant_count": 183, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-bug", - "listable": true, - "enabled": true, - "badge_grouping_id": 7, - "system": false, - "badge_type_id": 3 + id: 114, + name: "Bug Reporter", + description: + 'Reported a bug that was liked by the Discourse team', + grant_count: 183, + allow_title: false, + multiple_grant: false, + icon: "fa-bug", + listable: true, + enabled: true, + badge_grouping_id: 7, + system: false, + badge_type_id: 3 }, { - "id": 4, - "name": "Elder", - "description": null, - "grant_count": 4, - "allow_title": true, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 4, - "system": true, - "badge_type_id": 1 + id: 4, + name: "Elder", + description: null, + grant_count: 4, + allow_title: true, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + badge_type_id: 1 }, { - "id": 17, - "name": "Reader", - "description": null, - "grant_count": 278, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 17, + name: "Reader", + description: null, + grant_count: 278, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 }, { - "id": 1, - "name": "Basic User", - "description": null, - "grant_count": 5834, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 4, - "system": true, - "badge_type_id": 3 + id: 1, + name: "Basic User", + description: null, + grant_count: 5834, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + badge_type_id: 3 }, { - "id": 16, - "name": "Read Guidelines", - "description": null, - "grant_count": 60, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 16, + name: "Read Guidelines", + description: null, + grant_count: 60, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 }, { - "id": 7, - "name": "Good Post", - "description": null, - "grant_count": 22, - "allow_title": false, - "multiple_grant": true, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 3, - "system": true, - "badge_type_id": 2 + id: 7, + name: "Good Post", + description: null, + grant_count: 22, + allow_title: false, + multiple_grant: true, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 3, + system: true, + badge_type_id: 2 }, { - "id": 8, - "name": "Great Post", - "description": null, - "grant_count": 2, - "allow_title": false, - "multiple_grant": true, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 3, - "system": true, - "badge_type_id": 1 + id: 8, + name: "Great Post", + description: null, + grant_count: 2, + allow_title: false, + multiple_grant: true, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 3, + system: true, + badge_type_id: 1 }, { - "id": 11, - "name": "First Like", - "description": null, - "grant_count": 2387, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 11, + name: "First Like", + description: null, + grant_count: 2387, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 }, { - "id": 12, - "name": "First Share", - "description": null, - "grant_count": 285, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 12, + name: "First Share", + description: null, + grant_count: 285, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 }, { - "id": 13, - "name": "First Flag", - "description": null, - "grant_count": 42, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 2, - "system": true, - "badge_type_id": 3 + id: 13, + name: "First Flag", + description: null, + grant_count: 42, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 2, + system: true, + badge_type_id: 3 }, { - "id": 5, - "name": "Welcome", - "description": null, - "grant_count": 1718, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 2, - "system": true, - "badge_type_id": 3 + id: 5, + name: "Welcome", + description: null, + grant_count: 1718, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 2, + system: true, + badge_type_id: 3 }, { - "id": 15, - "name": "First Quote", - "description": null, - "grant_count": 270, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 15, + name: "First Quote", + description: null, + grant_count: 270, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 }, { - "id": 9, - "name": "Autobiographer", - "description": null, - "long_description": "hello", - "grant_count": 545, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 9, + name: "Autobiographer", + description: null, + long_description: "hello", + grant_count: 545, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 }, { - "id": 14, - "name": "First Link", - "description": null, - "grant_count": 397, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 14, + name: "First Link", + description: null, + grant_count: 397, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 }, { - "id": 6, - "name": "Nice Post", - "description": null, - "grant_count": 259, - "allow_title": false, - "multiple_grant": true, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 3, - "system": true, - "badge_type_id": 3 + id: 6, + name: "Nice Post", + description: null, + grant_count: 259, + allow_title: false, + multiple_grant: true, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 3, + system: true, + badge_type_id: 3 }, { - "id": 10, - "name": "Editor", - "description": null, - "grant_count": 933, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 2, - "system": true, - "badge_type_id": 3 + id: 10, + name: "Editor", + description: null, + grant_count: 933, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 2, + system: true, + badge_type_id: 3 } ] }, "/badges/9": { - "badge_types": [ + badge_types: [ { - "id": 3, - "name": "Bronze" + id: 3, + name: "Bronze" } ], - "badge": { - "id": 9, - "name": "Autobiographer", - "description": null, - "long_description": "", - "grant_count": 545, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + badge: { + id: 9, + name: "Autobiographer", + description: null, + long_description: "", + grant_count: 545, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 } }, "/user_badges.json": { - "badges": [ + badges: [ { - "id": 9, - "name": "Autobiographer", - "description": null, - "grant_count": 545, - "allow_title": false, - "multiple_grant": false, - "icon": "fa-certificate", - "listable": true, - "enabled": true, - "badge_grouping_id": 1, - "system": true, - "badge_type_id": 3 + id: 9, + name: "Autobiographer", + description: null, + grant_count: 545, + allow_title: false, + multiple_grant: false, + icon: "fa-certificate", + listable: true, + enabled: true, + badge_grouping_id: 1, + system: true, + badge_type_id: 3 } ], - "badge_types": [ + badge_types: [ { - "id": 3, - "name": "Bronze" + id: 3, + name: "Bronze" } ], - "users": [ + users: [ { - "id": 11209, - "username": "icaroperseo", - "uploaded_avatar_id": 33076, - "avatar_template":"/images/avatar.png" + id: 11209, + username: "icaroperseo", + uploaded_avatar_id: 33076, + avatar_template: "/images/avatar.png" }, { - "id": -1, - "username": "system", - "uploaded_avatar_id": 5241, - "avatar_template":"/images/avatar.png" + id: -1, + username: "system", + uploaded_avatar_id: 5241, + avatar_template: "/images/avatar.png" }, { - "id": 11234, - "username": "allard", - "uploaded_avatar_id": 33117, - "avatar_template":"/images/avatar.png" + id: 11234, + username: "allard", + uploaded_avatar_id: 33117, + avatar_template: "/images/avatar.png" }, { - "id": 8944, - "username": "hunterboerner", - "uploaded_avatar_id": 33072, - "avatar_template":"/images/avatar.png" + id: 8944, + username: "hunterboerner", + uploaded_avatar_id: 33072, + avatar_template: "/images/avatar.png" }, { - "id": 11232, - "username": "daydreamer", - "uploaded_avatar_id": 33101, - "avatar_template":"/images/avatar.png" + id: 11232, + username: "daydreamer", + uploaded_avatar_id: 33101, + avatar_template: "/images/avatar.png" }, { - "id": 11160, - "username": "boomzilla", - "uploaded_avatar_id": 33029, - "avatar_template":"/images/avatar.png" + id: 11160, + username: "boomzilla", + uploaded_avatar_id: 33029, + avatar_template: "/images/avatar.png" }, { - "id": 5303, - "username": "ybart", - "uploaded_avatar_id": 14132, - "avatar_template":"/images/avatar.png" + id: 5303, + username: "ybart", + uploaded_avatar_id: 14132, + avatar_template: "/images/avatar.png" }, { - "id": 11142, - "username": "Fluffy", - "uploaded_avatar_id": 32957, - "avatar_template":"/images/avatar.png" + id: 11142, + username: "Fluffy", + uploaded_avatar_id: 32957, + avatar_template: "/images/avatar.png" }, { - "id": 8843, - "username": "timoroso", - "uploaded_avatar_id": 19114, - "avatar_template":"/images/avatar.png" + id: 8843, + username: "timoroso", + uploaded_avatar_id: 19114, + avatar_template: "/images/avatar.png" }, { - "id": 10990, - "username": "Nagesh", - "uploaded_avatar_id": 32736, - "avatar_template":"/images/avatar.png" + id: 10990, + username: "Nagesh", + uploaded_avatar_id: 32736, + avatar_template: "/images/avatar.png" }, { - "id": 11027, - "username": "dullroar", - "uploaded_avatar_id": 32801, - "avatar_template":"/images/avatar.png" + id: 11027, + username: "dullroar", + uploaded_avatar_id: 32801, + avatar_template: "/images/avatar.png" }, { - "id": 10481, - "username": "Air_Cooled_Nut", - "uploaded_avatar_id": 31833, - "avatar_template":"/images/avatar.png" + id: 10481, + username: "Air_Cooled_Nut", + uploaded_avatar_id: 31833, + avatar_template: "/images/avatar.png" }, { - "id": 10977, - "username": "stevebridger", - "uploaded_avatar_id": 32705, - "avatar_template":"/images/avatar.png" + id: 10977, + username: "stevebridger", + uploaded_avatar_id: 32705, + avatar_template: "/images/avatar.png" }, { - "id": 10921, - "username": "lnikkila", - "uploaded_avatar_id": 32627, - "avatar_template":"/images/avatar.png" + id: 10921, + username: "lnikkila", + uploaded_avatar_id: 32627, + avatar_template: "/images/avatar.png" }, { - "id": 8493, - "username": "PJH", - "uploaded_avatar_id": 33082, - "avatar_template":"/images/avatar.png" + id: 8493, + username: "PJH", + uploaded_avatar_id: 33082, + avatar_template: "/images/avatar.png" }, { - "id": 10635, - "username": "Ganzuelo", - "uploaded_avatar_id": 32217, - "avatar_template":"/images/avatar.png" + id: 10635, + username: "Ganzuelo", + uploaded_avatar_id: 32217, + avatar_template: "/images/avatar.png" }, { - "id": 8300, - "username": "cpradio", - "uploaded_avatar_id": 4970, - "avatar_template":"/images/avatar.png" + id: 8300, + username: "cpradio", + uploaded_avatar_id: 4970, + avatar_template: "/images/avatar.png" }, { - "id": 8571, - "username": "tobiaseigen", - "uploaded_avatar_id": 9785, - "avatar_template":"/images/avatar.png" + id: 8571, + username: "tobiaseigen", + uploaded_avatar_id: 9785, + avatar_template: "/images/avatar.png" }, { - "id": 4263, - "username": "mcwumbly", - "uploaded_avatar_id": 9796, - "avatar_template":"/images/avatar.png" + id: 4263, + username: "mcwumbly", + uploaded_avatar_id: 9796, + avatar_template: "/images/avatar.png" }, { - "id": 471, - "username": "BhaelOchon", - "uploaded_avatar_id": 6069, - "avatar_template":"/images/avatar.png" + id: 471, + username: "BhaelOchon", + uploaded_avatar_id: 6069, + avatar_template: "/images/avatar.png" }, { - "id": 5249, - "username": "cawas", - "uploaded_avatar_id": 14043, - "avatar_template":"/images/avatar.png" + id: 5249, + username: "cawas", + uploaded_avatar_id: 14043, + avatar_template: "/images/avatar.png" }, { - "id": 5461, - "username": "thepractice", - "uploaded_avatar_id": 2397, - "avatar_template":"/images/avatar.png" + id: 5461, + username: "thepractice", + uploaded_avatar_id: 2397, + avatar_template: "/images/avatar.png" }, { - "id": 10467, - "username": "chris18890", - "uploaded_avatar_id": 31806, - "avatar_template":"/images/avatar.png" + id: 10467, + username: "chris18890", + uploaded_avatar_id: 31806, + avatar_template: "/images/avatar.png" }, { - "id": 375, - "username": "weirdcanada", - "uploaded_avatar_id": 5902, - "avatar_template":"/images/avatar.png" + id: 375, + username: "weirdcanada", + uploaded_avatar_id: 5902, + avatar_template: "/images/avatar.png" }, { - "id": 8617, - "username": "Mittineague", - "uploaded_avatar_id": 4462, - "avatar_template":"/images/avatar.png" + id: 8617, + username: "Mittineague", + uploaded_avatar_id: 4462, + avatar_template: "/images/avatar.png" }, { - "id": 5962, - "username": "TheMarkus", - "uploaded_avatar_id": 15186, - "avatar_template":"/images/avatar.png" + id: 5962, + username: "TheMarkus", + uploaded_avatar_id: 15186, + avatar_template: "/images/avatar.png" }, { - "id": 2806, - "username": "fayimora", - "uploaded_avatar_id": 10007, - "avatar_template":"/images/avatar.png" + id: 2806, + username: "fayimora", + uploaded_avatar_id: 10007, + avatar_template: "/images/avatar.png" }, { - "id": 8364, - "username": "codetricity", - "uploaded_avatar_id": 3773, - "avatar_template":"/images/avatar.png" + id: 8364, + username: "codetricity", + uploaded_avatar_id: 3773, + avatar_template: "/images/avatar.png" }, { - "id": 3752, - "username": "liberatiluca", - "uploaded_avatar_id": 11568, - "avatar_template":"/images/avatar.png" + id: 3752, + username: "liberatiluca", + uploaded_avatar_id: 11568, + avatar_template: "/images/avatar.png" }, { - "id": 3483, - "username": "Packetknife", - "uploaded_avatar_id": 11144, - "avatar_template":"/images/avatar.png" + id: 3483, + username: "Packetknife", + uploaded_avatar_id: 11144, + avatar_template: "/images/avatar.png" }, { - "id": 32, - "username": "codinghorror", - "uploaded_avatar_id": 5297, - "avatar_template":"/images/avatar.png" + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: "/images/avatar.png" }, { - "id": 19, - "username": "eviltrout", - "uploaded_avatar_id": 5275, - "avatar_template":"/images/avatar.png" + id: 19, + username: "eviltrout", + uploaded_avatar_id: 5275, + avatar_template: "/images/avatar.png" }, { - "id": 7229, - "username": "DavidGNavas", - "uploaded_avatar_id": 17081, - "avatar_template":"/images/avatar.png" + id: 7229, + username: "DavidGNavas", + uploaded_avatar_id: 17081, + avatar_template: "/images/avatar.png" }, { - "id": 1219, - "username": "Gweebz", - "uploaded_avatar_id": 7304, - "avatar_template":"/images/avatar.png" + id: 1219, + username: "Gweebz", + uploaded_avatar_id: 7304, + avatar_template: "/images/avatar.png" }, { - "id": 7743, - "username": "ZeroFlux", - "uploaded_avatar_id": 2256, - "avatar_template":"/images/avatar.png" + id: 7743, + username: "ZeroFlux", + uploaded_avatar_id: 2256, + avatar_template: "/images/avatar.png" }, { - "id": 8510, - "username": "tannerfilip", - "uploaded_avatar_id": 18674, - "avatar_template":"/images/avatar.png" + id: 8510, + username: "tannerfilip", + uploaded_avatar_id: 18674, + avatar_template: "/images/avatar.png" }, { - "id": 1496, - "username": "cfstras", - "uploaded_avatar_id": 7776, - "avatar_template":"/images/avatar.png" + id: 1496, + username: "cfstras", + uploaded_avatar_id: 7776, + avatar_template: "/images/avatar.png" }, { - "id": 3986, - "username": "creativetech", - "uploaded_avatar_id": 11955, - "avatar_template":"/images/avatar.png" + id: 3986, + username: "creativetech", + uploaded_avatar_id: 11955, + avatar_template: "/images/avatar.png" }, { - "id": 3800, - "username": "stealthii", - "uploaded_avatar_id": 11645, - "avatar_template":"/images/avatar.png" + id: 3800, + username: "stealthii", + uploaded_avatar_id: 11645, + avatar_template: "/images/avatar.png" }, { - "id": 6613, - "username": "haiku", - "uploaded_avatar_id": 9781, - "avatar_template":"/images/avatar.png" + id: 6613, + username: "haiku", + uploaded_avatar_id: 9781, + avatar_template: "/images/avatar.png" }, { - "id": 5351, - "username": "erlend_sh", - "uploaded_avatar_id": 9794, - "avatar_template":"/images/avatar.png" + id: 5351, + username: "erlend_sh", + uploaded_avatar_id: 9794, + avatar_template: "/images/avatar.png" }, { - "id": 5983, - "username": "JohnSReid", - "uploaded_avatar_id": 32238, - "avatar_template":"/images/avatar.png" + id: 5983, + username: "JohnSReid", + uploaded_avatar_id: 32238, + avatar_template: "/images/avatar.png" }, { - "id": 701, - "username": "johncoder", - "uploaded_avatar_id": 6447, - "avatar_template":"/images/avatar.png" + id: 701, + username: "johncoder", + uploaded_avatar_id: 6447, + avatar_template: "/images/avatar.png" }, { - "id": 5707, - "username": "trident", - "uploaded_avatar_id": 31178, - "avatar_template":"/images/avatar.png" + id: 5707, + username: "trident", + uploaded_avatar_id: 31178, + avatar_template: "/images/avatar.png" }, { - "id": 255, - "username": "uwe_keim", - "uploaded_avatar_id": 5697, - "avatar_template":"/images/avatar.png" + id: 255, + username: "uwe_keim", + uploaded_avatar_id: 5697, + avatar_template: "/images/avatar.png" }, { - "id": 9931, - "username": "Frank", - "uploaded_avatar_id": 32861, - "avatar_template":"/images/avatar.png" + id: 9931, + username: "Frank", + uploaded_avatar_id: 32861, + avatar_template: "/images/avatar.png" }, { - "id": 5543, - "username": "trevor", - "uploaded_avatar_id": 14507, - "avatar_template":"/images/avatar.png" + id: 5543, + username: "trevor", + uploaded_avatar_id: 14507, + avatar_template: "/images/avatar.png" }, { - "id": 3987, - "username": "Sander78", - "uploaded_avatar_id": 9787, - "avatar_template":"/images/avatar.png" + id: 3987, + username: "Sander78", + uploaded_avatar_id: 9787, + avatar_template: "/images/avatar.png" }, { - "id": 7850, - "username": "tudorv", - "uploaded_avatar_id": 2568, - "avatar_template":"/images/avatar.png" + id: 7850, + username: "tudorv", + uploaded_avatar_id: 2568, + avatar_template: "/images/avatar.png" }, { - "id": 6653, - "username": "amitfrid", - "uploaded_avatar_id": 16262, - "avatar_template":"/images/avatar.png" + id: 6653, + username: "amitfrid", + uploaded_avatar_id: 16262, + avatar_template: "/images/avatar.png" }, { - "id": 4419, - "username": "sasivarnakumar", - "uploaded_avatar_id": 12661, - "avatar_template":"/images/avatar.png" + id: 4419, + username: "sasivarnakumar", + uploaded_avatar_id: 12661, + avatar_template: "/images/avatar.png" }, { - "id": 5710, - "username": "elvanja", - "uploaded_avatar_id": 14781, - "avatar_template":"/images/avatar.png" + id: 5710, + username: "elvanja", + uploaded_avatar_id: 14781, + avatar_template: "/images/avatar.png" }, { - "id": 5401, - "username": "nilaykumar", - "uploaded_avatar_id": 14275, - "avatar_template":"/images/avatar.png" + id: 5401, + username: "nilaykumar", + uploaded_avatar_id: 14275, + avatar_template: "/images/avatar.png" }, { - "id": 6809, - "username": "buster", - "uploaded_avatar_id": 31175, - "avatar_template":"/images/avatar.png" + id: 6809, + username: "buster", + uploaded_avatar_id: 31175, + avatar_template: "/images/avatar.png" }, { - "id": 169, - "username": "blowmage", - "uploaded_avatar_id": 5545, - "avatar_template":"/images/avatar.png" + id: 169, + username: "blowmage", + uploaded_avatar_id: 5545, + avatar_template: "/images/avatar.png" }, { - "id": 766, - "username": "dworthley", - "uploaded_avatar_id": 6561, - "avatar_template":"/images/avatar.png" + id: 766, + username: "dworthley", + uploaded_avatar_id: 6561, + avatar_template: "/images/avatar.png" }, { - "id": 1612, - "username": "trottier", - "uploaded_avatar_id": 7977, - "avatar_template":"/images/avatar.png" + id: 1612, + username: "trottier", + uploaded_avatar_id: 7977, + avatar_template: "/images/avatar.png" }, { - "id": 6019, - "username": "mandie", - "uploaded_avatar_id": 15273, - "avatar_template":"/images/avatar.png" + id: 6019, + username: "mandie", + uploaded_avatar_id: 15273, + avatar_template: "/images/avatar.png" }, { - "id": 3724, - "username": "Manikin75", - "uploaded_avatar_id": 11520, - "avatar_template":"/images/avatar.png" + id: 3724, + username: "Manikin75", + uploaded_avatar_id: 11520, + avatar_template: "/images/avatar.png" }, { - "id": 1556, - "username": "OfferKaye", - "uploaded_avatar_id": 7878, - "avatar_template":"/images/avatar.png" + id: 1556, + username: "OfferKaye", + uploaded_avatar_id: 7878, + avatar_template: "/images/avatar.png" }, { - "id": 4063, - "username": "blanco", - "uploaded_avatar_id": 12082, - "avatar_template":"/images/avatar.png" + id: 4063, + username: "blanco", + uploaded_avatar_id: 12082, + avatar_template: "/images/avatar.png" }, { - "id": 1621, - "username": "bnb", - "uploaded_avatar_id": 7992, - "avatar_template":"/images/avatar.png" + id: 1621, + username: "bnb", + uploaded_avatar_id: 7992, + avatar_template: "/images/avatar.png" }, { - "id": 3095, - "username": "ayush", - "uploaded_avatar_id": 10504, - "avatar_template":"/images/avatar.png" + id: 3095, + username: "ayush", + uploaded_avatar_id: 10504, + avatar_template: "/images/avatar.png" }, { - "id": 754, - "username": "danneu", - "uploaded_avatar_id": 6540, - "avatar_template":"/images/avatar.png" + id: 754, + username: "danneu", + uploaded_avatar_id: 6540, + avatar_template: "/images/avatar.png" }, { - "id": 6548, - "username": "michaeld", - "uploaded_avatar_id": 1594, - "avatar_template":"/images/avatar.png" + id: 6548, + username: "michaeld", + uploaded_avatar_id: 1594, + avatar_template: "/images/avatar.png" }, { - "id": 4457, - "username": "Lee_Ars", - "uploaded_avatar_id": 1597, - "avatar_template":"/images/avatar.png" + id: 4457, + username: "Lee_Ars", + uploaded_avatar_id: 1597, + avatar_template: "/images/avatar.png" }, { - "id": 5160, - "username": "eriko", - "uploaded_avatar_id": 1915, - "avatar_template":"/images/avatar.png" + id: 5160, + username: "eriko", + uploaded_avatar_id: 1915, + avatar_template: "/images/avatar.png" }, { - "id": 10150, - "username": "ampburner", - "uploaded_avatar_id": 5103, - "avatar_template":"/images/avatar.png" + id: 10150, + username: "ampburner", + uploaded_avatar_id: 5103, + avatar_template: "/images/avatar.png" }, { - "id": 1, - "username": "sam", - "uploaded_avatar_id": 5243, - "avatar_template":"/images/avatar.png" + id: 1, + username: "sam", + uploaded_avatar_id: 5243, + avatar_template: "/images/avatar.png" }, { - "id": 1995, - "username": "zogstrip", - "uploaded_avatar_id": 8630, - "avatar_template":"/images/avatar.png" + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: "/images/avatar.png" }, { - "id": 9536, - "username": "nahtnam", - "uploaded_avatar_id": 20077, - "avatar_template":"/images/avatar.png" + id: 9536, + username: "nahtnam", + uploaded_avatar_id: 20077, + avatar_template: "/images/avatar.png" }, { - "id": 5559, - "username": "downey", - "uploaded_avatar_id": 14532, - "avatar_template":"/images/avatar.png" + id: 5559, + username: "downey", + uploaded_avatar_id: 14532, + avatar_template: "/images/avatar.png" }, { - "id": 6626, - "username": "riking", - "uploaded_avatar_id": 9779, - "avatar_template":"/images/avatar.png" + id: 6626, + username: "riking", + uploaded_avatar_id: 9779, + avatar_template: "/images/avatar.png" }, { - "id": 562, - "username": "nightpool", - "uploaded_avatar_id": 6220, - "avatar_template":"/images/avatar.png" + id: 562, + username: "nightpool", + uploaded_avatar_id: 6220, + avatar_template: "/images/avatar.png" }, { - "id": 2770, - "username": "awesomerobot", - "uploaded_avatar_id": 32393, - "avatar_template":"/images/avatar.png" + id: 2770, + username: "awesomerobot", + uploaded_avatar_id: 32393, + avatar_template: "/images/avatar.png" }, { - "id": 4385, - "username": "jeans", - "uploaded_avatar_id": 12606, - "avatar_template":"/images/avatar.png" + id: 4385, + username: "jeans", + uploaded_avatar_id: 12606, + avatar_template: "/images/avatar.png" }, { - "id": 8222, - "username": "techAPJ", - "uploaded_avatar_id": 3281, - "avatar_template":"/images/avatar.png" + id: 8222, + username: "techAPJ", + uploaded_avatar_id: 3281, + avatar_template: "/images/avatar.png" }, { - "id": 1274, - "username": "binaryphile", - "uploaded_avatar_id": 7399, - "avatar_template":"/images/avatar.png" + id: 1274, + username: "binaryphile", + uploaded_avatar_id: 7399, + avatar_template: "/images/avatar.png" }, { - "id": 15, - "username": "Hanzo", - "uploaded_avatar_id": 5267, - "avatar_template":"/images/avatar.png" + id: 15, + username: "Hanzo", + uploaded_avatar_id: 5267, + avatar_template: "/images/avatar.png" }, { - "id": 5199, - "username": "sefier", - "uploaded_avatar_id": 31207, - "avatar_template":"/images/avatar.png" + id: 5199, + username: "sefier", + uploaded_avatar_id: 31207, + avatar_template: "/images/avatar.png" }, { - "id": 2316, - "username": "pakl", - "uploaded_avatar_id": 9157, - "avatar_template":"/images/avatar.png" + id: 2316, + username: "pakl", + uploaded_avatar_id: 9157, + avatar_template: "/images/avatar.png" }, { - "id": 393, - "username": "freney", - "uploaded_avatar_id": 5932, - "avatar_template":"/images/avatar.png" + id: 393, + username: "freney", + uploaded_avatar_id: 5932, + avatar_template: "/images/avatar.png" }, { - "id": 8492, - "username": "Onaldan", - "uploaded_avatar_id": 18651, - "avatar_template":"/images/avatar.png" + id: 8492, + username: "Onaldan", + uploaded_avatar_id: 18651, + avatar_template: "/images/avatar.png" }, { - "id": 5002, - "username": "jakeberger", - "uploaded_avatar_id": 13630, - "avatar_template":"/images/avatar.png" + id: 5002, + username: "jakeberger", + uploaded_avatar_id: 13630, + avatar_template: "/images/avatar.png" }, { - "id": 2544, - "username": "davideyre", - "uploaded_avatar_id": 9543, - "avatar_template":"/images/avatar.png" + id: 2544, + username: "davideyre", + uploaded_avatar_id: 9543, + avatar_template: "/images/avatar.png" }, { - "id": 8342, - "username": "sethuv", - "uploaded_avatar_id": 3036, - "avatar_template":"/images/avatar.png" + id: 8342, + username: "sethuv", + uploaded_avatar_id: 3036, + avatar_template: "/images/avatar.png" }, { - "id": 1128, - "username": "Tigraine", - "uploaded_avatar_id": 7152, - "avatar_template":"/images/avatar.png" + id: 1128, + username: "Tigraine", + uploaded_avatar_id: 7152, + avatar_template: "/images/avatar.png" }, { - "id": 2477, - "username": "billybonks", - "uploaded_avatar_id": 9430, - "avatar_template":"/images/avatar.png" + id: 2477, + username: "billybonks", + uploaded_avatar_id: 9430, + avatar_template: "/images/avatar.png" }, { - "id": 4549, - "username": "davidcelis", - "uploaded_avatar_id": 12882, - "avatar_template":"/images/avatar.png" + id: 4549, + username: "davidcelis", + uploaded_avatar_id: 12882, + avatar_template: "/images/avatar.png" }, { - "id": 7264, - "username": "etrowbridge", - "uploaded_avatar_id": 31199, - "avatar_template":"/images/avatar.png" + id: 7264, + username: "etrowbridge", + uploaded_avatar_id: 31199, + avatar_template: "/images/avatar.png" }, { - "id": 413, - "username": "adam_baldwin", - "uploaded_avatar_id": 5962, - "avatar_template":"/images/avatar.png" + id: 413, + username: "adam_baldwin", + uploaded_avatar_id: 5962, + avatar_template: "/images/avatar.png" }, { - "id": 8658, - "username": "Datachick", - "uploaded_avatar_id": 18865, - "avatar_template":"/images/avatar.png" + id: 8658, + username: "Datachick", + uploaded_avatar_id: 18865, + avatar_template: "/images/avatar.png" }, { - "id": 5294, - "username": "madbomber", - "uploaded_avatar_id": 14118, - "avatar_template":"/images/avatar.png" + id: 5294, + username: "madbomber", + uploaded_avatar_id: 14118, + avatar_template: "/images/avatar.png" }, { - "id": 4750, - "username": "dainbinder", - "uploaded_avatar_id": 13220, - "avatar_template":"/images/avatar.png" + id: 4750, + username: "dainbinder", + uploaded_avatar_id: 13220, + avatar_template: "/images/avatar.png" }, { - "id": 2735, - "username": "royce_williams", - "uploaded_avatar_id": 9887, - "avatar_template":"/images/avatar.png" + id: 2735, + username: "royce_williams", + uploaded_avatar_id: 9887, + avatar_template: "/images/avatar.png" }, { - "id": 9089, - "username": "Keezer", - "uploaded_avatar_id": 31186, - "avatar_template":"/images/avatar.png" + id: 9089, + username: "Keezer", + uploaded_avatar_id: 31186, + avatar_template: "/images/avatar.png" } ], - "user_badges": [ + user_badges: [ { - "id": 39085, - "granted_at": "2014-07-30T20:06:09.461-04:00", - "badge_id": 9, - "user_id": 11209, - "granted_by_id": -1 + id: 39085, + granted_at: "2014-07-30T20:06:09.461-04:00", + badge_id: 9, + user_id: 11209, + granted_by_id: -1 }, { - "id": 39035, - "granted_at": "2014-07-29T15:26:57.028-04:00", - "badge_id": 9, - "user_id": 11234, - "granted_by_id": -1 + id: 39035, + granted_at: "2014-07-29T15:26:57.028-04:00", + badge_id: 9, + user_id: 11234, + granted_by_id: -1 }, { - "id": 39034, - "granted_at": "2014-07-29T14:28:43.359-04:00", - "badge_id": 9, - "user_id": 8944, - "granted_by_id": -1 + id: 39034, + granted_at: "2014-07-29T14:28:43.359-04:00", + badge_id: 9, + user_id: 8944, + granted_by_id: -1 }, { - "id": 39027, - "granted_at": "2014-07-29T13:14:40.612-04:00", - "badge_id": 9, - "user_id": 11232, - "granted_by_id": -1 + id: 39027, + granted_at: "2014-07-29T13:14:40.612-04:00", + badge_id: 9, + user_id: 11232, + granted_by_id: -1 }, { - "id": 38874, - "granted_at": "2014-07-25T14:39:57.034-04:00", - "badge_id": 9, - "user_id": 11160, - "granted_by_id": -1 + id: 38874, + granted_at: "2014-07-25T14:39:57.034-04:00", + badge_id: 9, + user_id: 11160, + granted_by_id: -1 }, { - "id": 38866, - "granted_at": "2014-07-25T05:10:33.585-04:00", - "badge_id": 9, - "user_id": 5303, - "granted_by_id": -1 + id: 38866, + granted_at: "2014-07-25T05:10:33.585-04:00", + badge_id: 9, + user_id: 5303, + granted_by_id: -1 }, { - "id": 38838, - "granted_at": "2014-07-24T11:57:21.389-04:00", - "badge_id": 9, - "user_id": 11142, - "granted_by_id": -1 + id: 38838, + granted_at: "2014-07-24T11:57:21.389-04:00", + badge_id: 9, + user_id: 11142, + granted_by_id: -1 }, { - "id": 37659, - "granted_at": "2014-07-22T03:11:21.336-04:00", - "badge_id": 9, - "user_id": 8843, - "granted_by_id": -1 + id: 37659, + granted_at: "2014-07-22T03:11:21.336-04:00", + badge_id: 9, + user_id: 8843, + granted_by_id: -1 }, { - "id": 37611, - "granted_at": "2014-07-21T13:02:19.724-04:00", - "badge_id": 9, - "user_id": 10990, - "granted_by_id": -1 + id: 37611, + granted_at: "2014-07-21T13:02:19.724-04:00", + badge_id: 9, + user_id: 10990, + granted_by_id: -1 }, { - "id": 37537, - "granted_at": "2014-07-19T13:56:59.699-04:00", - "badge_id": 9, - "user_id": 11027, - "granted_by_id": -1 + id: 37537, + granted_at: "2014-07-19T13:56:59.699-04:00", + badge_id: 9, + user_id: 11027, + granted_by_id: -1 }, { - "id": 37497, - "granted_at": "2014-07-18T10:03:45.294-04:00", - "badge_id": 9, - "user_id": 10481, - "granted_by_id": -1 + id: 37497, + granted_at: "2014-07-18T10:03:45.294-04:00", + badge_id: 9, + user_id: 10481, + granted_by_id: -1 }, { - "id": 37217, - "granted_at": "2014-07-15T15:29:45.464-04:00", - "badge_id": 9, - "user_id": 10977, - "granted_by_id": -1 + id: 37217, + granted_at: "2014-07-15T15:29:45.464-04:00", + badge_id: 9, + user_id: 10977, + granted_by_id: -1 }, { - "id": 36865, - "granted_at": "2014-07-12T08:45:09.386-04:00", - "badge_id": 9, - "user_id": 10921, - "granted_by_id": -1 + id: 36865, + granted_at: "2014-07-12T08:45:09.386-04:00", + badge_id: 9, + user_id: 10921, + granted_by_id: -1 }, { - "id": 36126, - "granted_at": "2014-07-09T08:02:43.143-04:00", - "badge_id": 9, - "user_id": 8493, - "granted_by_id": -1 + id: 36126, + granted_at: "2014-07-09T08:02:43.143-04:00", + badge_id: 9, + user_id: 8493, + granted_by_id: -1 }, { - "id": 32551, - "granted_at": "2014-07-06T00:07:44.455-04:00", - "badge_id": 9, - "user_id": 10635, - "granted_by_id": -1 + id: 32551, + granted_at: "2014-07-06T00:07:44.455-04:00", + badge_id: 9, + user_id: 10635, + granted_by_id: -1 }, { - "id": 32550, - "granted_at": "2014-07-05T17:45:16.661-04:00", - "badge_id": 9, - "user_id": 8300, - "granted_by_id": -1 + id: 32550, + granted_at: "2014-07-05T17:45:16.661-04:00", + badge_id: 9, + user_id: 8300, + granted_by_id: -1 }, { - "id": 15327, - "granted_at": "2014-07-04T16:03:28.852-04:00", - "badge_id": 9, - "user_id": 8571, - "granted_by_id": -1 + id: 15327, + granted_at: "2014-07-04T16:03:28.852-04:00", + badge_id: 9, + user_id: 8571, + granted_by_id: -1 }, { - "id": 7787, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 4263, - "granted_by_id": -1 + id: 7787, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 4263, + granted_by_id: -1 }, { - "id": 7786, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 471, - "granted_by_id": -1 + id: 7786, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 471, + granted_by_id: -1 }, { - "id": 7785, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5249, - "granted_by_id": -1 + id: 7785, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5249, + granted_by_id: -1 }, { - "id": 7784, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5461, - "granted_by_id": -1 + id: 7784, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5461, + granted_by_id: -1 }, { - "id": 7783, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 10467, - "granted_by_id": -1 + id: 7783, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 10467, + granted_by_id: -1 }, { - "id": 7782, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 375, - "granted_by_id": -1 + id: 7782, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 375, + granted_by_id: -1 }, { - "id": 7781, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 8617, - "granted_by_id": -1 + id: 7781, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 8617, + granted_by_id: -1 }, { - "id": 7780, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5962, - "granted_by_id": -1 + id: 7780, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5962, + granted_by_id: -1 }, { - "id": 7779, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 2806, - "granted_by_id": -1 + id: 7779, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 2806, + granted_by_id: -1 }, { - "id": 7778, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 8364, - "granted_by_id": -1 + id: 7778, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 8364, + granted_by_id: -1 }, { - "id": 7777, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 3752, - "granted_by_id": -1 + id: 7777, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 3752, + granted_by_id: -1 }, { - "id": 7776, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 3483, - "granted_by_id": -1 + id: 7776, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 3483, + granted_by_id: -1 }, { - "id": 7775, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 32, - "granted_by_id": -1 + id: 7775, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 32, + granted_by_id: -1 }, { - "id": 7774, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 19, - "granted_by_id": -1 + id: 7774, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 19, + granted_by_id: -1 }, { - "id": 7773, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 7229, - "granted_by_id": -1 + id: 7773, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 7229, + granted_by_id: -1 }, { - "id": 7772, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1219, - "granted_by_id": -1 + id: 7772, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1219, + granted_by_id: -1 }, { - "id": 7771, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 7743, - "granted_by_id": -1 + id: 7771, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 7743, + granted_by_id: -1 }, { - "id": 7770, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 8510, - "granted_by_id": -1 + id: 7770, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 8510, + granted_by_id: -1 }, { - "id": 7769, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1496, - "granted_by_id": -1 + id: 7769, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1496, + granted_by_id: -1 }, { - "id": 7768, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 3986, - "granted_by_id": -1 + id: 7768, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 3986, + granted_by_id: -1 }, { - "id": 7767, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 3800, - "granted_by_id": -1 + id: 7767, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 3800, + granted_by_id: -1 }, { - "id": 7766, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 6613, - "granted_by_id": -1 + id: 7766, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 6613, + granted_by_id: -1 }, { - "id": 7765, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5351, - "granted_by_id": -1 + id: 7765, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5351, + granted_by_id: -1 }, { - "id": 7764, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5983, - "granted_by_id": -1 + id: 7764, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5983, + granted_by_id: -1 }, { - "id": 7763, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 701, - "granted_by_id": -1 + id: 7763, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 701, + granted_by_id: -1 }, { - "id": 7762, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5707, - "granted_by_id": -1 + id: 7762, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5707, + granted_by_id: -1 }, { - "id": 7761, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 255, - "granted_by_id": -1 + id: 7761, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 255, + granted_by_id: -1 }, { - "id": 7760, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 9931, - "granted_by_id": -1 + id: 7760, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 9931, + granted_by_id: -1 }, { - "id": 7759, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": -1, - "granted_by_id": -1 + id: 7759, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: -1, + granted_by_id: -1 }, { - "id": 7758, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5543, - "granted_by_id": -1 + id: 7758, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5543, + granted_by_id: -1 }, { - "id": 7757, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 3987, - "granted_by_id": -1 + id: 7757, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 3987, + granted_by_id: -1 }, { - "id": 7756, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 7850, - "granted_by_id": -1 + id: 7756, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 7850, + granted_by_id: -1 }, { - "id": 7755, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 6653, - "granted_by_id": -1 + id: 7755, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 6653, + granted_by_id: -1 }, { - "id": 7754, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 4419, - "granted_by_id": -1 + id: 7754, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 4419, + granted_by_id: -1 }, { - "id": 7753, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5710, - "granted_by_id": -1 + id: 7753, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5710, + granted_by_id: -1 }, { - "id": 7752, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5401, - "granted_by_id": -1 + id: 7752, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5401, + granted_by_id: -1 }, { - "id": 7751, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 6809, - "granted_by_id": -1 + id: 7751, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 6809, + granted_by_id: -1 }, { - "id": 7750, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 169, - "granted_by_id": -1 + id: 7750, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 169, + granted_by_id: -1 }, { - "id": 7749, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 766, - "granted_by_id": -1 + id: 7749, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 766, + granted_by_id: -1 }, { - "id": 7748, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1612, - "granted_by_id": -1 + id: 7748, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1612, + granted_by_id: -1 }, { - "id": 7747, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 6019, - "granted_by_id": -1 + id: 7747, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 6019, + granted_by_id: -1 }, { - "id": 7746, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 3724, - "granted_by_id": -1 + id: 7746, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 3724, + granted_by_id: -1 }, { - "id": 7745, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1556, - "granted_by_id": -1 + id: 7745, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1556, + granted_by_id: -1 }, { - "id": 7744, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 4063, - "granted_by_id": -1 + id: 7744, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 4063, + granted_by_id: -1 }, { - "id": 7743, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1621, - "granted_by_id": -1 + id: 7743, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1621, + granted_by_id: -1 }, { - "id": 7742, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 3095, - "granted_by_id": -1 + id: 7742, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 3095, + granted_by_id: -1 }, { - "id": 7741, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 754, - "granted_by_id": -1 + id: 7741, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 754, + granted_by_id: -1 }, { - "id": 7740, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 6548, - "granted_by_id": -1 + id: 7740, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 6548, + granted_by_id: -1 }, { - "id": 7739, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 4457, - "granted_by_id": -1 + id: 7739, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 4457, + granted_by_id: -1 }, { - "id": 7738, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5160, - "granted_by_id": -1 + id: 7738, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5160, + granted_by_id: -1 }, { - "id": 7737, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 10150, - "granted_by_id": -1 + id: 7737, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 10150, + granted_by_id: -1 }, { - "id": 7736, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1, - "granted_by_id": -1 + id: 7736, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1, + granted_by_id: -1 }, { - "id": 7735, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1995, - "granted_by_id": -1 + id: 7735, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1995, + granted_by_id: -1 }, { - "id": 7734, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 9536, - "granted_by_id": -1 + id: 7734, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 9536, + granted_by_id: -1 }, { - "id": 7733, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5559, - "granted_by_id": -1 + id: 7733, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5559, + granted_by_id: -1 }, { - "id": 7732, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 6626, - "granted_by_id": -1 + id: 7732, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 6626, + granted_by_id: -1 }, { - "id": 7731, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 562, - "granted_by_id": -1 + id: 7731, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 562, + granted_by_id: -1 }, { - "id": 7730, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 2770, - "granted_by_id": -1 + id: 7730, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 2770, + granted_by_id: -1 }, { - "id": 7729, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 4385, - "granted_by_id": -1 + id: 7729, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 4385, + granted_by_id: -1 }, { - "id": 7728, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 8222, - "granted_by_id": -1 + id: 7728, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 8222, + granted_by_id: -1 }, { - "id": 7727, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1274, - "granted_by_id": -1 + id: 7727, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1274, + granted_by_id: -1 }, { - "id": 7726, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 15, - "granted_by_id": -1 + id: 7726, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 15, + granted_by_id: -1 }, { - "id": 7725, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5199, - "granted_by_id": -1 + id: 7725, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5199, + granted_by_id: -1 }, { - "id": 7724, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 2316, - "granted_by_id": -1 + id: 7724, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 2316, + granted_by_id: -1 }, { - "id": 7723, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 393, - "granted_by_id": -1 + id: 7723, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 393, + granted_by_id: -1 }, { - "id": 7722, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 8492, - "granted_by_id": -1 + id: 7722, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 8492, + granted_by_id: -1 }, { - "id": 7721, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5002, - "granted_by_id": -1 + id: 7721, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5002, + granted_by_id: -1 }, { - "id": 7720, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 2544, - "granted_by_id": -1 + id: 7720, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 2544, + granted_by_id: -1 }, { - "id": 7719, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 8342, - "granted_by_id": -1 + id: 7719, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 8342, + granted_by_id: -1 }, { - "id": 7718, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 1128, - "granted_by_id": -1 + id: 7718, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 1128, + granted_by_id: -1 }, { - "id": 7717, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 2477, - "granted_by_id": -1 + id: 7717, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 2477, + granted_by_id: -1 }, { - "id": 7716, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 4549, - "granted_by_id": -1 + id: 7716, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 4549, + granted_by_id: -1 }, { - "id": 7715, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 7264, - "granted_by_id": -1 + id: 7715, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 7264, + granted_by_id: -1 }, { - "id": 7714, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 413, - "granted_by_id": -1 + id: 7714, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 413, + granted_by_id: -1 }, { - "id": 7713, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 8658, - "granted_by_id": -1 + id: 7713, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 8658, + granted_by_id: -1 }, { - "id": 7712, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 5294, - "granted_by_id": -1 + id: 7712, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 5294, + granted_by_id: -1 }, { - "id": 7711, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 4750, - "granted_by_id": -1 + id: 7711, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 4750, + granted_by_id: -1 }, { - "id": 7710, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 2735, - "granted_by_id": -1 + id: 7710, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 2735, + granted_by_id: -1 }, { - "id": 7709, - "granted_at": "2014-07-03T06:36:46.939-04:00", - "badge_id": 9, - "user_id": 9089, - "granted_by_id": -1 + id: 7709, + granted_at: "2014-07-03T06:36:46.939-04:00", + badge_id: 9, + user_id: 9089, + granted_by_id: -1 } ] } diff --git a/test/javascripts/fixtures/category-fixtures.js.es6 b/test/javascripts/fixtures/category-fixtures.js.es6 index 1769e488da..ea1842a71a 100644 --- a/test/javascripts/fixtures/category-fixtures.js.es6 +++ b/test/javascripts/fixtures/category-fixtures.js.es6 @@ -1,3 +1,43 @@ export default { - "/c/1/show.json": {"category":{"id":1,"name":"bug","color":"e9dd00","text_color":"000000","slug":"bug","topic_count":2030,"post_count":13418,"description":"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","description_text":"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","topic_url":"/t/category-definition-for-bug/2","read_restricted":false,"permission":null,"notification_level":null,"logo_url":null,"background_url":null,"available_groups":["admins","discourse","everyone","moderators","staff","translators","trust_level_0","trust_level_1","trust_level_2","trust_level_3","trust_level_4"],"auto_close_hours":null,"auto_close_based_on_last_post":false,"group_permissions":[{"permission_type":1,"group_name":"everyone"}],"position":25,"cannot_delete_reason":"Can't delete this category because it has 2030 topics. Oldest topic is When a new post appears in a topic, the bookmark isn't updated.","allow_badges":true}} + "/c/1/show.json": { + category: { + id: 1, + name: "bug", + color: "e9dd00", + text_color: "000000", + slug: "bug", + topic_count: 2030, + post_count: 13418, + description: + "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", + description_text: + "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", + topic_url: "/t/category-definition-for-bug/2", + read_restricted: false, + permission: null, + notification_level: null, + logo_url: null, + background_url: null, + available_groups: [ + "admins", + "discourse", + "everyone", + "moderators", + "staff", + "translators", + "trust_level_0", + "trust_level_1", + "trust_level_2", + "trust_level_3", + "trust_level_4" + ], + auto_close_hours: null, + auto_close_based_on_last_post: false, + group_permissions: [{ permission_type: 1, group_name: "everyone" }], + position: 25, + cannot_delete_reason: + "Can't delete this category because it has 2030 topics. Oldest topic is When a new post appears in a topic, the bookmark isn't updated.", + allow_badges: true + } + } }; diff --git a/test/javascripts/fixtures/daily-engaged-users.js.es6 b/test/javascripts/fixtures/daily-engaged-users.js.es6 index 4787c458f3..b734c47b74 100644 --- a/test/javascripts/fixtures/daily-engaged-users.js.es6 +++ b/test/javascripts/fixtures/daily-engaged-users.js.es6 @@ -1,20 +1,20 @@ export default { "/admin/reports/daily_engaged_users": { - "report": { - "type": "daily_engaged_users", - "title": "Daily Engaged Users", - "xaxis": "Day", - "yaxis": "Engaged Users", - "description": "Number of users that have liked or posted in the last day", - "data": null, - "total": null, - "start_date": "2018-04-03", - "end_date": "2018-05-03", - "category_id": null, - "group_id": null, - "prev30Days": null, - "labels": null, - "report_key": "" + report: { + type: "daily_engaged_users", + title: "Daily Engaged Users", + xaxis: "Day", + yaxis: "Engaged Users", + description: "Number of users that have liked or posted in the last day", + data: null, + total: null, + start_date: "2018-04-03", + end_date: "2018-05-03", + category_id: null, + group_id: null, + prev30Days: null, + labels: null, + report_key: "" } } }; diff --git a/test/javascripts/fixtures/dashboard-next.js.es6 b/test/javascripts/fixtures/dashboard-next.js.es6 index 91ff5ea2bf..f1dfb0b7d0 100644 --- a/test/javascripts/fixtures/dashboard-next.js.es6 +++ b/test/javascripts/fixtures/dashboard-next.js.es6 @@ -1,13 +1,13 @@ export default { "/admin/dashboard-next.json": { - "reports": [], - "last_backup_taken_at": "2018-04-13T12:51:19.926Z", - "updated_at": "2018-04-25T08:06:11.292Z", - "disk_space": { - "uploads_used": "74.5 KB", - "uploads_free": "117 GB", - "backups_used": "4.24 GB", - "backups_free": "117 GB" + reports: [], + last_backup_taken_at: "2018-04-13T12:51:19.926Z", + updated_at: "2018-04-25T08:06:11.292Z", + disk_space: { + uploads_used: "74.5 KB", + uploads_free: "117 GB", + backups_used: "4.24 GB", + backups_free: "117 GB" } } }; diff --git a/test/javascripts/fixtures/dau-by-mau.js.es6 b/test/javascripts/fixtures/dau-by-mau.js.es6 index 78a125d952..221486bcf6 100644 --- a/test/javascripts/fixtures/dau-by-mau.js.es6 +++ b/test/javascripts/fixtures/dau-by-mau.js.es6 @@ -1,20 +1,20 @@ export default { "/admin/reports/dau_by_mau": { - "report": { - "type": "dau_by_mau", - "title": "DAU/MAU", - "xaxis": "Day", - "yaxis": "DAU/MAY", - "description": "Percentage of daily active users on monthly active users", - "data": null, - "total": null, - "start_date": "2018-01-26T00:00:00.000Z", - "end_date": "2018-04-27T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": 46, - "labels": null, - "report_key": "" + report: { + type: "dau_by_mau", + title: "DAU/MAU", + xaxis: "Day", + yaxis: "DAU/MAY", + description: "Percentage of daily active users on monthly active users", + data: null, + total: null, + start_date: "2018-01-26T00:00:00.000Z", + end_date: "2018-04-27T23:59:59.999Z", + category_id: null, + group_id: null, + prev30Days: 46, + labels: null, + report_key: "" } } }; diff --git a/test/javascripts/fixtures/directory-fixtures.js.es6 b/test/javascripts/fixtures/directory-fixtures.js.es6 index 82ae8d6090..ab1b647f4e 100644 --- a/test/javascripts/fixtures/directory-fixtures.js.es6 +++ b/test/javascripts/fixtures/directory-fixtures.js.es6 @@ -1,3 +1,609 @@ export default { - "directory_items": {"directory_items":[{"id":32,"username":"codinghorror","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"55d","likes_received":9370,"likes_given":7725,"topics_entered":11453,"topic_count":184,"post_count":12263},{"id":1,"username":"sam","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"52d","likes_received":7834,"likes_given":2693,"topics_entered":11024,"topic_count":276,"post_count":7802},{"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"25d","likes_received":2383,"likes_given":319,"topics_entered":8041,"topic_count":34,"post_count":1602},{"id":6626,"username":"riking","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"17d","likes_received":2101,"likes_given":2756,"topics_entered":9055,"topic_count":163,"post_count":2548},{"id":1995,"username":"zogstrip","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"32d","likes_received":1838,"likes_given":4588,"topics_entered":10823,"topic_count":16,"post_count":2050},{"id":8300,"username":"cpradio","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"11d","likes_received":1538,"likes_given":1001,"topics_entered":6121,"topic_count":111,"post_count":1430},{"id":2,"username":"neil","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"24d","likes_received":1238,"likes_given":684,"topics_entered":3250,"topic_count":27,"post_count":969},{"id":4263,"username":"mcwumbly","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"15d","likes_received":1223,"likes_given":1296,"topics_entered":5924,"topic_count":81,"post_count":1031},{"id":5351,"username":"erlend_sh","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"9d","likes_received":1115,"likes_given":747,"topics_entered":3260,"topic_count":154,"post_count":721},{"id":5559,"username":"downey","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"5d","likes_received":983,"likes_given":1713,"topics_entered":2995,"topic_count":131,"post_count":850},{"id":2770,"username":"awesomerobot","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"9d","likes_received":952,"likes_given":195,"topics_entered":2411,"topic_count":13,"post_count":402},{"id":9775,"username":"elberet","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"7d","likes_received":930,"likes_given":159,"topics_entered":4077,"topic_count":28,"post_count":755},{"id":8222,"username":"techAPJ","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"12d","likes_received":791,"likes_given":1005,"topics_entered":3691,"topic_count":43,"post_count":463},{"id":6060,"username":"lightyear","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":708,"likes_given":330,"topics_entered":1717,"topic_count":34,"post_count":312},{"id":8,"username":"geek","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"20d","likes_received":634,"likes_given":152,"topics_entered":920,"topic_count":48,"post_count":298},{"id":464,"username":"DeanMarkTaylor","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"10d","likes_received":578,"likes_given":299,"topics_entered":2976,"topic_count":116,"post_count":485},{"id":11160,"username":"boomzilla","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"1d","likes_received":561,"likes_given":398,"topics_entered":822,"topic_count":23,"post_count":185},{"id":4457,"username":"Lee_Ars","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":530,"likes_given":163,"topics_entered":2250,"topic_count":46,"post_count":327},{"id":8571,"username":"tobiaseigen","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"6d","likes_received":524,"likes_given":1275,"topics_entered":2545,"topic_count":140,"post_count":435},{"id":3,"username":"supermathie","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"20d","likes_received":510,"likes_given":312,"topics_entered":1733,"topic_count":62,"post_count":438},{"id":8493,"username":"PJH","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":458,"likes_given":96,"topics_entered":1219,"topic_count":74,"post_count":318},{"id":8617,"username":"Mittineague","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"16d","likes_received":415,"likes_given":291,"topics_entered":6662,"topic_count":22,"post_count":757},{"id":10778,"username":"Lid","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"8d","likes_received":398,"likes_given":296,"topics_entered":1771,"topic_count":82,"post_count":307},{"id":6548,"username":"michaeld","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":387,"likes_given":165,"topics_entered":1407,"topic_count":48,"post_count":330},{"id":471,"username":"BhaelOchon","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"22d","likes_received":386,"likes_given":765,"topics_entered":8051,"topic_count":55,"post_count":486},{"id":4881,"username":"gerhard","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"6d","likes_received":360,"likes_given":393,"topics_entered":3030,"topic_count":57,"post_count":250},{"id":5707,"username":"trident","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"6d","likes_received":344,"likes_given":181,"topics_entered":4905,"topic_count":2,"post_count":549},{"id":3987,"username":"Sander78","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":326,"likes_given":276,"topics_entered":3613,"topic_count":94,"post_count":392},{"id":3415,"username":"radq","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":299,"likes_given":110,"topics_entered":2503,"topic_count":16,"post_count":157},{"id":2989,"username":"meglio","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":280,"likes_given":436,"topics_entered":1086,"topic_count":198,"post_count":458},{"id":4,"username":"stienman","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"15d","likes_received":276,"likes_given":100,"topics_entered":291,"topic_count":13,"post_count":132},{"id":10855,"username":"abarker","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"1d","likes_received":270,"likes_given":131,"topics_entered":703,"topic_count":14,"post_count":77},{"id":9653,"username":"TechnoBear","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"4d","likes_received":263,"likes_given":507,"topics_entered":2931,"topic_count":51,"post_count":220},{"id":7948,"username":"probus","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"6d","likes_received":261,"likes_given":71,"topics_entered":2399,"topic_count":51,"post_count":206},{"id":9741,"username":"chapel","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"2d","likes_received":239,"likes_given":169,"topics_entered":1228,"topic_count":11,"post_count":167},{"id":8810,"username":"fantasticfears","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"4d","likes_received":213,"likes_given":184,"topics_entered":2161,"topic_count":29,"post_count":227},{"id":38,"username":"frandallfarmer","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"19d","likes_received":212,"likes_given":104,"topics_entered":3169,"topic_count":6,"post_count":114},{"id":8085,"username":"watchmanmonitor","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"2d","likes_received":202,"likes_given":654,"topics_entered":1453,"topic_count":73,"post_count":278},{"id":8909,"username":"AdamCapriola","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":200,"likes_given":179,"topics_entered":1689,"topic_count":49,"post_count":169},{"id":14,"username":"clay","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"14d","likes_received":183,"likes_given":103,"topics_entered":780,"topic_count":24,"post_count":97},{"id":6613,"username":"haiku","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":183,"likes_given":308,"topics_entered":1919,"topic_count":33,"post_count":188},{"id":13132,"username":"purldator","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"4d","likes_received":178,"likes_given":685,"topics_entered":1891,"topic_count":20,"post_count":299},{"id":810,"username":"ChrisHanel","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"16d","likes_received":169,"likes_given":42,"topics_entered":639,"topic_count":9,"post_count":94},{"id":2625,"username":"kpfleming","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"10d","likes_received":165,"likes_given":288,"topics_entered":2539,"topic_count":15,"post_count":233},{"id":7717,"username":"lake54","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"2d","likes_received":148,"likes_given":440,"topics_entered":1604,"topic_count":33,"post_count":194},{"id":8018,"username":"shivermetimbers","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"15h","likes_received":139,"likes_given":40,"topics_entered":185,"topic_count":30,"post_count":181},{"id":2316,"username":"pakl","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"14d","likes_received":135,"likes_given":198,"topics_entered":1034,"topic_count":46,"post_count":130},{"id":3681,"username":"Ajarn","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"3d","likes_received":126,"likes_given":664,"topics_entered":1893,"topic_count":46,"post_count":291},{"id":7229,"username":"DavidGNavas","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"4d","likes_received":124,"likes_given":210,"topics_entered":2032,"topic_count":17,"post_count":60},{"id":3062,"username":"Sailsman63","uploaded_avatar_id":null,"avatar_template":"/images/avatar.png","time_read":"8d","likes_received":124,"likes_given":139,"topics_entered":4257,"topic_count":10,"post_count":146}],"total_rows_directory_items":12546,"load_more_directory_items":"/directory_items?id=all&order=likes_received&page=1"} + directory_items: { + directory_items: [ + { + id: 32, + username: "codinghorror", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "55d", + likes_received: 9370, + likes_given: 7725, + topics_entered: 11453, + topic_count: 184, + post_count: 12263 + }, + { + id: 1, + username: "sam", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "52d", + likes_received: 7834, + likes_given: 2693, + topics_entered: 11024, + topic_count: 276, + post_count: 7802 + }, + { + id: 19, + username: "eviltrout", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "25d", + likes_received: 2383, + likes_given: 319, + topics_entered: 8041, + topic_count: 34, + post_count: 1602 + }, + { + id: 6626, + username: "riking", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "17d", + likes_received: 2101, + likes_given: 2756, + topics_entered: 9055, + topic_count: 163, + post_count: 2548 + }, + { + id: 1995, + username: "zogstrip", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "32d", + likes_received: 1838, + likes_given: 4588, + topics_entered: 10823, + topic_count: 16, + post_count: 2050 + }, + { + id: 8300, + username: "cpradio", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "11d", + likes_received: 1538, + likes_given: 1001, + topics_entered: 6121, + topic_count: 111, + post_count: 1430 + }, + { + id: 2, + username: "neil", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "24d", + likes_received: 1238, + likes_given: 684, + topics_entered: 3250, + topic_count: 27, + post_count: 969 + }, + { + id: 4263, + username: "mcwumbly", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "15d", + likes_received: 1223, + likes_given: 1296, + topics_entered: 5924, + topic_count: 81, + post_count: 1031 + }, + { + id: 5351, + username: "erlend_sh", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "9d", + likes_received: 1115, + likes_given: 747, + topics_entered: 3260, + topic_count: 154, + post_count: 721 + }, + { + id: 5559, + username: "downey", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "5d", + likes_received: 983, + likes_given: 1713, + topics_entered: 2995, + topic_count: 131, + post_count: 850 + }, + { + id: 2770, + username: "awesomerobot", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "9d", + likes_received: 952, + likes_given: 195, + topics_entered: 2411, + topic_count: 13, + post_count: 402 + }, + { + id: 9775, + username: "elberet", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "7d", + likes_received: 930, + likes_given: 159, + topics_entered: 4077, + topic_count: 28, + post_count: 755 + }, + { + id: 8222, + username: "techAPJ", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "12d", + likes_received: 791, + likes_given: 1005, + topics_entered: 3691, + topic_count: 43, + post_count: 463 + }, + { + id: 6060, + username: "lightyear", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 708, + likes_given: 330, + topics_entered: 1717, + topic_count: 34, + post_count: 312 + }, + { + id: 8, + username: "geek", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "20d", + likes_received: 634, + likes_given: 152, + topics_entered: 920, + topic_count: 48, + post_count: 298 + }, + { + id: 464, + username: "DeanMarkTaylor", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "10d", + likes_received: 578, + likes_given: 299, + topics_entered: 2976, + topic_count: 116, + post_count: 485 + }, + { + id: 11160, + username: "boomzilla", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "1d", + likes_received: 561, + likes_given: 398, + topics_entered: 822, + topic_count: 23, + post_count: 185 + }, + { + id: 4457, + username: "Lee_Ars", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 530, + likes_given: 163, + topics_entered: 2250, + topic_count: 46, + post_count: 327 + }, + { + id: 8571, + username: "tobiaseigen", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "6d", + likes_received: 524, + likes_given: 1275, + topics_entered: 2545, + topic_count: 140, + post_count: 435 + }, + { + id: 3, + username: "supermathie", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "20d", + likes_received: 510, + likes_given: 312, + topics_entered: 1733, + topic_count: 62, + post_count: 438 + }, + { + id: 8493, + username: "PJH", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 458, + likes_given: 96, + topics_entered: 1219, + topic_count: 74, + post_count: 318 + }, + { + id: 8617, + username: "Mittineague", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "16d", + likes_received: 415, + likes_given: 291, + topics_entered: 6662, + topic_count: 22, + post_count: 757 + }, + { + id: 10778, + username: "Lid", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "8d", + likes_received: 398, + likes_given: 296, + topics_entered: 1771, + topic_count: 82, + post_count: 307 + }, + { + id: 6548, + username: "michaeld", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 387, + likes_given: 165, + topics_entered: 1407, + topic_count: 48, + post_count: 330 + }, + { + id: 471, + username: "BhaelOchon", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "22d", + likes_received: 386, + likes_given: 765, + topics_entered: 8051, + topic_count: 55, + post_count: 486 + }, + { + id: 4881, + username: "gerhard", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "6d", + likes_received: 360, + likes_given: 393, + topics_entered: 3030, + topic_count: 57, + post_count: 250 + }, + { + id: 5707, + username: "trident", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "6d", + likes_received: 344, + likes_given: 181, + topics_entered: 4905, + topic_count: 2, + post_count: 549 + }, + { + id: 3987, + username: "Sander78", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 326, + likes_given: 276, + topics_entered: 3613, + topic_count: 94, + post_count: 392 + }, + { + id: 3415, + username: "radq", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 299, + likes_given: 110, + topics_entered: 2503, + topic_count: 16, + post_count: 157 + }, + { + id: 2989, + username: "meglio", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 280, + likes_given: 436, + topics_entered: 1086, + topic_count: 198, + post_count: 458 + }, + { + id: 4, + username: "stienman", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "15d", + likes_received: 276, + likes_given: 100, + topics_entered: 291, + topic_count: 13, + post_count: 132 + }, + { + id: 10855, + username: "abarker", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "1d", + likes_received: 270, + likes_given: 131, + topics_entered: 703, + topic_count: 14, + post_count: 77 + }, + { + id: 9653, + username: "TechnoBear", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "4d", + likes_received: 263, + likes_given: 507, + topics_entered: 2931, + topic_count: 51, + post_count: 220 + }, + { + id: 7948, + username: "probus", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "6d", + likes_received: 261, + likes_given: 71, + topics_entered: 2399, + topic_count: 51, + post_count: 206 + }, + { + id: 9741, + username: "chapel", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "2d", + likes_received: 239, + likes_given: 169, + topics_entered: 1228, + topic_count: 11, + post_count: 167 + }, + { + id: 8810, + username: "fantasticfears", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "4d", + likes_received: 213, + likes_given: 184, + topics_entered: 2161, + topic_count: 29, + post_count: 227 + }, + { + id: 38, + username: "frandallfarmer", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "19d", + likes_received: 212, + likes_given: 104, + topics_entered: 3169, + topic_count: 6, + post_count: 114 + }, + { + id: 8085, + username: "watchmanmonitor", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "2d", + likes_received: 202, + likes_given: 654, + topics_entered: 1453, + topic_count: 73, + post_count: 278 + }, + { + id: 8909, + username: "AdamCapriola", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 200, + likes_given: 179, + topics_entered: 1689, + topic_count: 49, + post_count: 169 + }, + { + id: 14, + username: "clay", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "14d", + likes_received: 183, + likes_given: 103, + topics_entered: 780, + topic_count: 24, + post_count: 97 + }, + { + id: 6613, + username: "haiku", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 183, + likes_given: 308, + topics_entered: 1919, + topic_count: 33, + post_count: 188 + }, + { + id: 13132, + username: "purldator", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "4d", + likes_received: 178, + likes_given: 685, + topics_entered: 1891, + topic_count: 20, + post_count: 299 + }, + { + id: 810, + username: "ChrisHanel", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "16d", + likes_received: 169, + likes_given: 42, + topics_entered: 639, + topic_count: 9, + post_count: 94 + }, + { + id: 2625, + username: "kpfleming", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "10d", + likes_received: 165, + likes_given: 288, + topics_entered: 2539, + topic_count: 15, + post_count: 233 + }, + { + id: 7717, + username: "lake54", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "2d", + likes_received: 148, + likes_given: 440, + topics_entered: 1604, + topic_count: 33, + post_count: 194 + }, + { + id: 8018, + username: "shivermetimbers", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "15h", + likes_received: 139, + likes_given: 40, + topics_entered: 185, + topic_count: 30, + post_count: 181 + }, + { + id: 2316, + username: "pakl", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "14d", + likes_received: 135, + likes_given: 198, + topics_entered: 1034, + topic_count: 46, + post_count: 130 + }, + { + id: 3681, + username: "Ajarn", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "3d", + likes_received: 126, + likes_given: 664, + topics_entered: 1893, + topic_count: 46, + post_count: 291 + }, + { + id: 7229, + username: "DavidGNavas", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "4d", + likes_received: 124, + likes_given: 210, + topics_entered: 2032, + topic_count: 17, + post_count: 60 + }, + { + id: 3062, + username: "Sailsman63", + uploaded_avatar_id: null, + avatar_template: "/images/avatar.png", + time_read: "8d", + likes_received: 124, + likes_given: 139, + topics_entered: 4257, + topic_count: 10, + post_count: 146 + } + ], + total_rows_directory_items: 12546, + load_more_directory_items: + "/directory_items?id=all&order=likes_received&page=1" + } }; diff --git a/test/javascripts/fixtures/discovery_fixtures.js.es6 b/test/javascripts/fixtures/discovery_fixtures.js.es6 index 8f0acc137d..3f1dc63adb 100644 --- a/test/javascripts/fixtures/discovery_fixtures.js.es6 +++ b/test/javascripts/fixtures/discovery_fixtures.js.es6 @@ -1,9 +1,6014 @@ /*jshint maxlen:10000000 */ export default { -"/latest.json": {"users":[{"id":7204,"username":"reyman64","avatar_template":"/images/avatar.png"},{"id":1,"username":"sam","avatar_template":"/images/avatar.png"},{"id":5481,"username":"f0rkz","avatar_template":"/images/avatar.png"},{"id":6473,"username":"jkf","avatar_template":"/images/avatar.png"},{"id":6973,"username":"stellarhopper","avatar_template":"/images/avatar.png"},{"id":19,"username":"eviltrout","avatar_template":"/images/avatar.png"},{"id":14,"username":"clay","avatar_template":"/images/avatar.png"},{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"},{"id":1917,"username":"sil","avatar_template":"/images/avatar.png"},{"id":7197,"username":"peeja","avatar_template":"/images/avatar.png"},{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"},{"id":8021,"username":"Abhishek_Gupta","avatar_template":"/images/avatar.png"},{"id":2291,"username":"PabloC","avatar_template":"/images/avatar.png"},{"id":791,"username":"srid","avatar_template":"/images/avatar.png"},{"id":1580,"username":"ABillionSuns","avatar_template":"/images/avatar.png"},{"id":7270,"username":"mhurwi","avatar_template":"/images/avatar.png"},{"id":6695,"username":"illspirit","avatar_template":"/images/avatar.png"},{"id":6929,"username":"BCHK","avatar_template":"/images/avatar.png"},{"id":4385,"username":"jeans","avatar_template":"/images/avatar.png"},{"id":7073,"username":"5an1ty","avatar_template":"/images/avatar.png"},{"id":6626,"username":"riking","avatar_template":"/images/avatar.png"},{"id":4457,"username":"Lee_Ars","avatar_template":"/images/avatar.png"},{"id":4263,"username":"mcwumbly","avatar_template":"/images/avatar.png"},{"id":8134,"username":"iontishina","avatar_template":"/images/avatar.png"},{"id":2072,"username":"nXqd","avatar_template":"/images/avatar.png"},{"id":4983,"username":"hey_julien","avatar_template":"/images/avatar.png"},{"id":3657,"username":"steelmaiden","avatar_template":"/images/avatar.png"},{"id":2624,"username":"BowlingX","avatar_template":"/images/avatar.png"},{"id":8085,"username":"watchmanmonitor","avatar_template":"/images/avatar.png"},{"id":4612,"username":"Iszi","avatar_template":"/images/avatar.png"},{"id":8018,"username":"shivermetimbers","avatar_template":"/images/avatar.png"},{"id":6060,"username":"lightyear","avatar_template":"/images/avatar.png"},{"id":2,"username":"neil","avatar_template":"/images/avatar.png"},{"id":8037,"username":"printec","avatar_template":"/images/avatar.png"},{"id":3415,"username":"radq","avatar_template":"/images/avatar.png"},{"id":6283,"username":"hrishikesh","avatar_template":"/images/avatar.png"},{"id":471,"username":"BhaelOchon","avatar_template":"/images/avatar.png"},{"id":6548,"username":"michaeld","avatar_template":"/images/avatar.png"},{"id":7286,"username":"mrotsnahoj","avatar_template":"/images/avatar.png"},{"id":3169,"username":"dgw","avatar_template":"/images/avatar.png"},{"id":926,"username":"martinnormark","avatar_template":"/images/avatar.png"},{"id":2003,"username":"taylor","avatar_template":"/images/avatar.png"},{"id":369,"username":"CvX","avatar_template":"/images/avatar.png"},{"id":562,"username":"nightpool","avatar_template":"/images/avatar.png"},{"id":6653,"username":"amitfrid","avatar_template":"/images/avatar.png"},{"id":6677,"username":"Tropnevad","avatar_template":"/images/avatar.png"},{"id":5048,"username":"SneakySly","avatar_template":"/images/avatar.png"},{"id":7333,"username":"Jong","avatar_template":"/images/avatar.png"},{"id":3124,"username":"sipp11","avatar_template":"/images/avatar.png"},{"id":7604,"username":"citkane","avatar_template":"/images/avatar.png"},{"id":3929,"username":"ScotterC","avatar_template":"/images/avatar.png"},{"id":6680,"username":"cdman","avatar_template":"/images/avatar.png"},{"id":500,"username":"aeid","avatar_template":"/images/avatar.png"},{"id":8,"username":"geek","avatar_template":"/images/avatar.png"},{"id":606,"username":"Cafeine","avatar_template":"/images/avatar.png"}],"topic_list":{"can_create_topic":false,"more_topics_url":"/latest.json?page=1","draft":null,"draft_key":"new_topic","draft_sequence":null,"topics":[{"id":11557,"title":"Error after upgrade to 0.9.7.9+","fancy_title":"Error after upgrade to 0.9.7.9+","slug":"error-after-upgrade-to-0-9-7-9","posts_count":83,"reply_count":58,"highest_post_number":85,"image_url":null,"created_at":"2013-12-22T17:12:05.000-05:00","last_posted_at":"2014-01-16T00:52:30.000-05:00","bumped":true,"bumped_at":"2014-01-16T00:52:30.000-05:00","unseen":false,"pinned":true,"excerpt":"Hi, \n\nI'm using webfaction postgresql specific private instance to run discourse (custom port already configured for discourse 0.9.7.6). \n\nThis is not my first update, but this time i have an error. Impossible to upgrade…","visible":true,"closed":false,"archived":false,"views":1230,"like_count":40,"has_summary":true,"archetype":"regular","last_poster_username":"stellarhopper","category_id":17,"posters":[{"extras":null,"description":"Original Poster","user_id":7204},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":5481},{"extras":null,"description":"Frequent Poster","user_id":6473},{"extras":"latest","description":"Most Recent Poster","user_id":6973}]},{"id":1,"title":"Welcome to meta.discourse.org","fancy_title":"Welcome to meta.discourse.org","slug":"welcome-to-meta-discourse-org","posts_count":5,"reply_count":5,"highest_post_number":23,"image_url":null,"created_at":"2013-01-31T23:52:28.000-05:00","last_posted_at":"2013-02-07T16:50:41.000-05:00","bumped":true,"bumped_at":"2013-02-07T11:57:34.000-05:00","unseen":false,"pinned":true,"excerpt":"Welcome to meta, the official site for discussing the next-gen open source Discourse forum software. You'll find topics on features, bugs, hosting, development, and general support here. \n\nDiscourse is early beta softwar…","visible":true,"closed":true,"archived":false,"views":13792,"like_count":108,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":17,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":null,"description":"Most Posts","user_id":19},{"extras":null,"description":"Frequent Poster","user_id":14},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11997,"title":"Create topic in the future","fancy_title":"Create topic in the future","slug":"create-topic-in-the-future","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T12:14:36.000-05:00","last_posted_at":"2014-01-16T12:14:36.000-05:00","bumped":false,"bumped_at":"2014-01-16T12:14:36.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":7,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sil","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":1917}]},{"id":11996,"title":"It's really hard to navigate the Create Topic / Reply pane with the keyboard","fancy_title":"It’s really hard to navigate the Create Topic / Reply pane with the keyboard","slug":"its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2014-01-16T10:51:36.000-05:00","last_posted_at":"2014-01-16T11:11:10.000-05:00","bumped":true,"bumped_at":"2014-01-16T11:11:10.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":12,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":9,"posters":[{"extras":null,"description":"Original Poster","user_id":7197},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":11994,"title":"Cross domain rules, followed?","fancy_title":"Cross domain rules, followed?","slug":"cross-domain-rules-followed","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-16T09:59:15.000-05:00","last_posted_at":"2014-01-16T09:59:15.000-05:00","bumped":true,"bumped_at":"2014-01-16T11:04:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":15,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Abhishek_Gupta","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":8021}]},{"id":11995,"title":"Discourse as a CAS Server","fancy_title":"Discourse as a CAS Server","slug":"discourse-as-a-cas-server","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T10:15:30.000-05:00","last_posted_at":"2014-01-16T10:15:31.000-05:00","bumped":true,"bumped_at":"2014-01-16T10:15:31.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":12,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"PabloC","category_id":6,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":2291}]},{"id":11993,"title":"How to check the user level via ajax?","fancy_title":"How to check the user level via ajax?","slug":"how-to-check-the-user-level-via-ajax","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T08:13:09.000-05:00","last_posted_at":"2014-01-16T08:13:09.000-05:00","bumped":true,"bumped_at":"2014-01-16T09:20:59.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":13,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Abhishek_Gupta","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":8021}]},{"id":9540,"title":"Docker images for Discourse","fancy_title":"Docker images for Discourse","slug":"docker-images-for-discourse","posts_count":35,"reply_count":28,"highest_post_number":36,"image_url":null,"created_at":"2013-09-02T00:07:02.000-04:00","last_posted_at":"2014-01-16T07:47:18.000-05:00","bumped":true,"bumped_at":"2014-01-16T07:47:18.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":1322,"like_count":23,"has_summary":false,"archetype":"regular","last_poster_username":"illspirit","category_id":8,"posters":[{"extras":null,"description":"Original Poster","user_id":791},{"extras":null,"description":"Most Posts","user_id":1580},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":7270},{"extras":"latest","description":"Most Recent Poster","user_id":6695}]},{"id":11957,"title":"Daily Active Users, Monthly Active Users - Statistics Need","fancy_title":"Daily Active Users, Monthly Active Users - Statistics Need","slug":"daily-active-users-monthly-active-users-statistics-need","posts_count":8,"reply_count":4,"highest_post_number":8,"image_url":null,"created_at":"2014-01-14T13:40:56.000-05:00","last_posted_at":"2014-01-16T06:46:05.000-05:00","bumped":true,"bumped_at":"2014-01-16T06:46:05.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":97,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"jeans","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6929},{"extras":null,"description":"Most Posts","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":4385}]},{"id":11973,"title":"Pressing Wrench Icon in the Categories section","fancy_title":"Pressing Wrench Icon in the Categories section","slug":"pressing-wrench-icon-in-the-categories-section","posts_count":6,"reply_count":3,"highest_post_number":6,"image_url":"/uploads/default/2907/d8d4e0accd5ee244.png","created_at":"2014-01-15T05:58:12.000-05:00","last_posted_at":"2014-01-16T05:15:52.000-05:00","bumped":true,"bumped_at":"2014-01-16T05:15:52.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":46,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"5an1ty","category_id":9,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":7073},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":6626}]},{"id":11835,"title":"The Road to Discourse 1.0","fancy_title":"The Road to Discourse 1.0","slug":"the-road-to-discourse-1-0","posts_count":6,"reply_count":2,"highest_post_number":6,"image_url":null,"created_at":"2014-01-08T19:08:44.000-05:00","last_posted_at":"2014-01-16T04:49:16.000-05:00","bumped":true,"bumped_at":"2014-01-16T04:49:16.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":421,"like_count":33,"has_summary":false,"archetype":"regular","last_poster_username":"iontishina","category_id":13,"posters":[{"extras":null,"description":"Original Poster","user_id":32},{"extras":null,"description":"Most Posts","user_id":4457},{"extras":null,"description":"Frequent Poster","user_id":4263},{"extras":"latest","description":"Most Recent Poster","user_id":8134}]},{"id":11992,"title":"Specific customization for each category","fancy_title":"Specific customization for each category","slug":"specific-customization-for-each-category","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T04:04:58.000-05:00","last_posted_at":"2014-01-16T04:04:58.000-05:00","bumped":false,"bumped_at":"2014-01-16T04:04:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":18,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"nXqd","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":2072}]},{"id":9214,"title":"Please make category url shorter","fancy_title":"Please make category url shorter","slug":"please-make-category-url-shorter","posts_count":9,"reply_count":3,"highest_post_number":9,"image_url":null,"created_at":"2013-08-20T05:28:17.000-04:00","last_posted_at":"2014-01-16T04:02:46.000-05:00","bumped":true,"bumped_at":"2014-01-16T04:02:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":319,"like_count":13,"has_summary":false,"archetype":"regular","last_poster_username":"nXqd","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":4983},{"extras":null,"description":"Most Posts","user_id":3657},{"extras":null,"description":"Frequent Poster","user_id":2624},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":2072}]},{"id":11989,"title":"Where to change the email subject prefix","fancy_title":"Where to change the email subject prefix","slug":"where-to-change-the-email-subject-prefix","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/2919/adbfe0ff90353440.png","created_at":"2014-01-16T01:03:48.000-05:00","last_posted_at":"2014-01-16T03:20:09.000-05:00","bumped":true,"bumped_at":"2014-01-16T03:20:09.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":19,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":6,"posters":[{"extras":null,"description":"Original Poster","user_id":8085},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":10866,"title":"Header logo overflows the top header area","fancy_title":"Header logo overflows the top header area","slug":"header-logo-overflows-the-top-header-area","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2013-11-09T03:40:04.000-05:00","last_posted_at":"2014-01-16T02:27:52.000-05:00","bumped":true,"bumped_at":"2014-01-16T02:40:47.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":157,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"stellarhopper","category_id":6,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6973},{"extras":null,"description":"Most Posts","user_id":32}]},{"id":11988,"title":"Could not locate Gemfile error","fancy_title":"Could not locate Gemfile error","slug":"could-not-locate-gemfile-error","posts_count":7,"reply_count":3,"highest_post_number":7,"image_url":null,"created_at":"2014-01-16T00:41:57.000-05:00","last_posted_at":"2014-01-16T01:20:46.000-05:00","bumped":true,"bumped_at":"2014-01-16T01:20:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":18,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":6,"posters":[{"extras":null,"description":"Original Poster","user_id":6973},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":6266,"title":"What sort of replies trigger a notice?","fancy_title":"What sort of replies trigger a notice?","slug":"what-sort-of-replies-trigger-a-notice","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2013-04-30T17:46:39.000-04:00","last_posted_at":"2014-01-16T00:52:21.000-05:00","bumped":true,"bumped_at":"2014-01-16T00:57:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":115,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":17,"posters":[{"extras":null,"description":"Original Poster","user_id":4612},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11610,"title":"Private replies that only admins can see","fancy_title":"Private replies that only admins can see","slug":"private-replies-that-only-admins-can-see","posts_count":21,"reply_count":20,"highest_post_number":23,"image_url":null,"created_at":"2013-12-26T20:31:10.000-05:00","last_posted_at":"2014-01-16T00:18:19.000-05:00","bumped":true,"bumped_at":"2014-01-16T00:18:19.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":206,"like_count":9,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":8018},{"extras":null,"description":"Most Posts","user_id":4263},{"extras":null,"description":"Frequent Poster","user_id":6060},{"extras":null,"description":"Frequent Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11888,"title":"Uncategorized topics not allowed, still seeing tag places","fancy_title":"Uncategorized topics not allowed, still seeing tag places","slug":"uncategorized-topics-not-allowed-still-seeing-tag-places","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2014-01-10T19:23:37.000-05:00","last_posted_at":"2014-01-15T22:41:25.000-05:00","bumped":true,"bumped_at":"2014-01-15T22:41:25.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":50,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"illspirit","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6695},{"extras":null,"description":"Most Posts","user_id":32},{"extras":null,"description":"Frequent Poster","user_id":2}]},{"id":11985,"title":"Installation nearly installs on Centos 6.5 with Apache/Phusion","fancy_title":"Installation nearly installs on Centos 6.5 with Apache/Phusion","slug":"installation-nearly-installs-on-centos-6-5-with-apache-phusion","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-15T19:48:30.000-05:00","last_posted_at":"2014-01-15T19:48:30.000-05:00","bumped":false,"bumped_at":"2014-01-15T19:48:30.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":26,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"printec","category_id":6,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":8037}]},{"id":11981,"title":"Excluding categories from the top view?","fancy_title":"Excluding categories from the top view?","slug":"excluding-categories-from-the-top-view","posts_count":6,"reply_count":1,"highest_post_number":6,"image_url":"/uploads/default/_optimized/f01/22f/7ea01f77b9_690x355.png","created_at":"2014-01-15T15:01:37.000-05:00","last_posted_at":"2014-01-15T18:57:52.000-05:00","bumped":true,"bumped_at":"2014-01-15T18:57:47.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":43,"like_count":6,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":3415},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":9408,"title":"Different home page for regular vs. new user","fancy_title":"Different home page for regular vs. new user","slug":"different-home-page-for-regular-vs-new-user","posts_count":25,"reply_count":17,"highest_post_number":25,"image_url":null,"created_at":"2013-08-28T09:54:41.000-04:00","last_posted_at":"2014-01-15T18:33:16.000-05:00","bumped":true,"bumped_at":"2014-01-15T18:33:16.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":334,"like_count":21,"has_summary":false,"archetype":"regular","last_poster_username":"mcwumbly","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6283},{"extras":null,"description":"Most Posts","user_id":32},{"extras":null,"description":"Frequent Poster","user_id":1995},{"extras":null,"description":"Frequent Poster","user_id":471},{"extras":"latest","description":"Most Recent Poster","user_id":4263}]},{"id":11896,"title":"Problem creating new account","fancy_title":"Problem creating new account","slug":"problem-creating-new-account","posts_count":11,"reply_count":2,"highest_post_number":11,"image_url":null,"created_at":"2014-01-11T09:07:20.000-05:00","last_posted_at":"2014-01-15T20:50:05.000-05:00","bumped":true,"bumped_at":"2014-01-15T15:23:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":87,"like_count":6,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":6,"posters":[{"extras":null,"description":"Original Poster","user_id":6548},{"extras":null,"description":"Most Posts","user_id":32},{"extras":null,"description":"Frequent Poster","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":10511,"title":"External urls should open in new tab","fancy_title":"External urls should open in new tab","slug":"external-urls-should-open-in-new-tab","posts_count":7,"reply_count":3,"highest_post_number":7,"image_url":null,"created_at":"2013-10-20T14:54:27.000-04:00","last_posted_at":"2014-01-15T14:02:11.000-05:00","bumped":true,"bumped_at":"2014-01-15T14:01:55.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":242,"like_count":10,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":7286},{"extras":null,"description":"Most Posts","user_id":3169},{"extras":null,"description":"Frequent Poster","user_id":4263},{"extras":null,"description":"Frequent Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":1589,"title":"Keyboard shortcuts?","fancy_title":"Keyboard shortcuts?","slug":"keyboard-shortcuts","posts_count":19,"reply_count":10,"highest_post_number":20,"image_url":null,"created_at":"2013-02-06T14:05:01.000-05:00","last_posted_at":"2014-01-15T13:52:45.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:52:45.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":754,"like_count":31,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":926},{"extras":null,"description":"Most Posts","user_id":2003},{"extras":null,"description":"Frequent Poster","user_id":369},{"extras":null,"description":"Frequent Poster","user_id":562},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11763,"title":"Google AdSense plugin is now available","fancy_title":"Google AdSense plugin is now available","slug":"google-adsense-plugin-is-now-available","posts_count":7,"reply_count":2,"highest_post_number":7,"image_url":"/uploads/default/_optimized/66d/cf0/d69e6709fe_496x500.PNG","created_at":"2014-01-05T14:28:58.000-05:00","last_posted_at":"2014-01-15T13:32:35.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:32:35.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":213,"like_count":14,"has_summary":false,"archetype":"regular","last_poster_username":"michaeld","category_id":5,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6548},{"extras":null,"description":"Most Posts","user_id":6653},{"extras":null,"description":"Frequent Poster","user_id":6677},{"extras":null,"description":"Frequent Poster","user_id":5048},{"extras":null,"description":"Frequent Poster","user_id":7333}]},{"id":9151,"title":"Apple touch icon doesn't show if there is no sub domain","fancy_title":"Apple touch icon doesn’t show if there is no sub domain","slug":"apple-touch-icon-doesnt-show-if-there-is-no-sub-domain","posts_count":7,"reply_count":4,"highest_post_number":7,"image_url":null,"created_at":"2013-08-16T18:16:53.000-04:00","last_posted_at":"2014-01-15T17:10:18.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:19:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":188,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":3124},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11977,"title":"Show subcategory topics in categories list summary","fancy_title":"Show subcategory topics in categories list summary","slug":"show-subcategory-topics-in-categories-list-summary","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/_optimized/084/4e4/8af88c0839_571x500.png","created_at":"2014-01-15T12:09:49.000-05:00","last_posted_at":"2014-01-15T12:50:04.000-05:00","bumped":true,"bumped_at":"2014-01-15T12:50:04.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":32,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":7604},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":10201,"title":"How To override an existing handlebars template from plugin","fancy_title":"How To override an existing handlebars template from plugin","slug":"how-to-override-an-existing-handlebars-template-from-plugin","posts_count":6,"reply_count":1,"highest_post_number":6,"image_url":null,"created_at":"2013-10-04T10:44:33.000-04:00","last_posted_at":"2014-01-15T12:35:01.000-05:00","bumped":true,"bumped_at":"2014-01-15T12:34:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":325,"like_count":6,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":3929},{"extras":null,"description":"Most Posts","user_id":3415},{"extras":null,"description":"Frequent Poster","user_id":6680},{"extras":null,"description":"Frequent Poster","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":531,"title":"Discourse and Wordpress Integration","fancy_title":"Discourse and Wordpress Integration","slug":"discourse-and-wordpress-integration","posts_count":76,"reply_count":64,"highest_post_number":78,"image_url":null,"created_at":"2013-02-05T18:56:37.000-05:00","last_posted_at":"2014-01-15T11:56:54.000-05:00","bumped":true,"bumped_at":"2014-01-15T11:56:54.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":3809,"like_count":84,"has_summary":true,"archetype":"regular","last_poster_username":"codinghorror","category_id":5,"posters":[{"extras":null,"description":"Original Poster","user_id":500},{"extras":null,"description":"Most Posts","user_id":8},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":606},{"extras":"latest","description":"Most Recent Poster","user_id":32}]}]}}, -"/categories.json": {"category_list":{"can_create_category":false,"can_create_topic":false,"draft":null,"draft_key":"new_topic","draft_sequence":null,"categories":[{"id":1,"name":"bug","color":"e9dd00","text_color":"000000","slug":"bug","topic_count":660,"description":"Bug reports on Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","topic_url":"/t/category-definition-for-bug/2","read_restricted":false,"permission":null,"post_count":4318,"topics_day":0,"topics_week":18,"topics_month":54,"topics_year":658,"posts_day":0,"posts_week":330,"posts_month":574,"posts_year":4319,"description_excerpt":"Bug reports on Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","featured_user_ids":[8021,32,6695,2,1995],"topics":[{"id":11994,"title":"Cross domain rules, followed?","fancy_title":"Cross domain rules, followed?","slug":"cross-domain-rules-followed","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-16T09:59:15.000-05:00","last_posted_at":"2014-01-16T09:59:15.000-05:00","bumped":true,"bumped_at":"2014-01-16T11:04:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":8021,"username":"Abhishek_Gupta","avatar_template":"/images/avatar.png"}},{"id":11888,"title":"Uncategorized topics not allowed, still seeing tag places","fancy_title":"Uncategorized topics not allowed, still seeing tag places","slug":"uncategorized-topics-not-allowed-still-seeing-tag-places","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2014-01-10T19:23:37.000-05:00","last_posted_at":"2014-01-15T22:41:25.000-05:00","bumped":true,"bumped_at":"2014-01-15T22:41:25.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":6695,"username":"illspirit","avatar_template":"/images/avatar.png"}},{"id":9151,"title":"Apple touch icon doesn't show if there is no sub domain","fancy_title":"Apple touch icon doesn’t show if there is no sub domain","slug":"apple-touch-icon-doesnt-show-if-there-is-no-sub-domain","posts_count":7,"reply_count":4,"highest_post_number":7,"image_url":null,"created_at":"2013-08-16T18:16:53.000-04:00","last_posted_at":"2014-01-15T17:10:18.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:19:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}}]},{"id":2,"name":"feature","color":"0E76BD","text_color":"FFFFFF","slug":"feature","topic_count":727,"description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","topic_url":"/t/category-definition-for-feature/11","read_restricted":false,"permission":null,"post_count":6186,"topics_day":0,"topics_week":17,"topics_month":46,"topics_year":725,"posts_day":0,"posts_week":180,"posts_month":468,"posts_year":6187,"description_excerpt":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","featured_user_ids":[1917,4385,2072,32,4263],"topics":[{"id":11997,"title":"Create topic in the future","fancy_title":"Create topic in the future","slug":"create-topic-in-the-future","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T12:14:36.000-05:00","last_posted_at":"2014-01-16T12:14:36.000-05:00","bumped":false,"bumped_at":"2014-01-16T12:14:36.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":1917,"username":"sil","avatar_template":"/images/avatar.png"}},{"id":11957,"title":"Daily Active Users, Monthly Active Users - Statistics Need","fancy_title":"Daily Active Users, Monthly Active Users - Statistics Need","slug":"daily-active-users-monthly-active-users-statistics-need","posts_count":8,"reply_count":4,"highest_post_number":8,"image_url":null,"created_at":"2014-01-14T13:40:56.000-05:00","last_posted_at":"2014-01-16T06:46:05.000-05:00","bumped":true,"bumped_at":"2014-01-16T06:46:05.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":4385,"username":"jeans","avatar_template":"/images/avatar.png"}},{"id":11992,"title":"Specific customization for each category","fancy_title":"Specific customization for each category","slug":"specific-customization-for-each-category","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T04:04:58.000-05:00","last_posted_at":"2014-01-16T04:04:58.000-05:00","bumped":false,"bumped_at":"2014-01-16T04:04:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":2072,"username":"nXqd","avatar_template":"/images/avatar.png"}}]},{"id":6,"name":"support","color":"b99","text_color":"FFFFFF","slug":"support","topic_count":782,"description":"Support on configuring, using, and installing Discourse. Not for software development related topics, but for admins and end users configuring and using Discourse.","topic_url":"/t/category-definition-for-support/389","read_restricted":false,"permission":null,"post_count":5396,"topics_day":0,"topics_week":16,"topics_month":67,"topics_year":779,"posts_day":0,"posts_week":122,"posts_month":481,"posts_year":5400,"description_excerpt":"Support on configuring, using, and installing Discourse. Not for software development related topics, but for admins and end users configuring and using Discourse.","featured_user_ids":[2291,32,6973,1,8085],"topics":[{"id":11995,"title":"Discourse as a CAS Server","fancy_title":"Discourse as a CAS Server","slug":"discourse-as-a-cas-server","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T10:15:30.000-05:00","last_posted_at":"2014-01-16T10:15:31.000-05:00","bumped":true,"bumped_at":"2014-01-16T10:15:31.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":2291,"username":"PabloC","avatar_template":"/images/avatar.png"}},{"id":11989,"title":"Where to change the email subject prefix","fancy_title":"Where to change the email subject prefix","slug":"where-to-change-the-email-subject-prefix","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/2919/adbfe0ff90353440.png","created_at":"2014-01-16T01:03:48.000-05:00","last_posted_at":"2014-01-16T03:20:09.000-05:00","bumped":true,"bumped_at":"2014-01-16T03:20:09.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}},{"id":10866,"title":"Header logo overflows the top header area","fancy_title":"Header logo overflows the top header area","slug":"header-logo-overflows-the-top-header-area","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2013-11-09T03:40:04.000-05:00","last_posted_at":"2014-01-16T02:27:52.000-05:00","bumped":true,"bumped_at":"2014-01-16T02:40:47.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":6973,"username":"stellarhopper","avatar_template":"/images/avatar.png"}}]},{"id":7,"name":"dev","color":"000","text_color":"FFFFFF","slug":"dev","topic_count":284,"description":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.","topic_url":"/t/category-definition-for-dev/1026","read_restricted":false,"permission":null,"post_count":2352,"topics_day":0,"topics_week":3,"topics_month":19,"topics_year":284,"posts_day":0,"posts_week":37,"posts_month":150,"posts_year":2353,"description_excerpt":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.","featured_user_ids":[8021,1995,5428,8208,7995],"topics":[{"id":3823,"title":"So, you want to help out with Discourse","fancy_title":"So, you want to help out with Discourse","slug":"so-you-want-to-help-out-with-discourse","posts_count":22,"reply_count":28,"highest_post_number":56,"image_url":null,"created_at":"2013-02-23T00:46:11.000-05:00","last_posted_at":"2014-01-12T21:33:12.000-05:00","bumped":true,"bumped_at":"2014-01-12T21:33:12.000-05:00","unseen":false,"pinned":true,"excerpt":"People are wondering, how it is they can help out with Discourse. \n\nWe have seen some chattering both here and on Github. \n\nI wanted to create a topic @eviltrout , @codinghorror and myself can keep up to date with clear…","visible":true,"closed":false,"archived":false,"last_poster":{"id":7995,"username":"Hunter","avatar_template":"/images/avatar.png"}},{"id":11993,"title":"How to check the user level via ajax?","fancy_title":"How to check the user level via ajax?","slug":"how-to-check-the-user-level-via-ajax","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-16T08:13:09.000-05:00","last_posted_at":"2014-01-16T08:13:09.000-05:00","bumped":true,"bumped_at":"2014-01-16T09:20:59.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":8021,"username":"Abhishek_Gupta","avatar_template":"/images/avatar.png"}},{"id":10201,"title":"How To override an existing handlebars template from plugin","fancy_title":"How To override an existing handlebars template from plugin","slug":"how-to-override-an-existing-handlebars-template-from-plugin","posts_count":6,"reply_count":1,"highest_post_number":6,"image_url":null,"created_at":"2013-10-04T10:44:33.000-04:00","last_posted_at":"2014-01-15T12:35:01.000-05:00","bumped":true,"bumped_at":"2014-01-15T12:34:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"last_poster":{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"}}]},{"id":9,"name":"ux","color":"5F497A","text_color":"FFFFFF","slug":"ux","topic_count":184,"description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","topic_url":"/t/category-definition-for-ux/2628","read_restricted":false,"permission":null,"post_count":1511,"topics_day":0,"topics_week":3,"topics_month":10,"topics_year":183,"posts_day":0,"posts_week":34,"posts_month":117,"posts_year":1511,"description_excerpt":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","featured_user_ids":[1995,7197,7073,1,6626],"topics":[{"id":11996,"title":"It's really hard to navigate the Create Topic / Reply pane with the keyboard","fancy_title":"It’s really hard to navigate the Create Topic / Reply pane with the keyboard","slug":"its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2014-01-16T10:51:36.000-05:00","last_posted_at":"2014-01-16T11:11:10.000-05:00","bumped":true,"bumped_at":"2014-01-16T11:11:10.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"}},{"id":11973,"title":"Pressing Wrench Icon in the Categories section","fancy_title":"Pressing Wrench Icon in the Categories section","slug":"pressing-wrench-icon-in-the-categories-section","posts_count":6,"reply_count":3,"highest_post_number":6,"image_url":"/uploads/default/2907/d8d4e0accd5ee244.png","created_at":"2014-01-15T05:58:12.000-05:00","last_posted_at":"2014-01-16T05:15:52.000-05:00","bumped":true,"bumped_at":"2014-01-16T05:15:52.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":7073,"username":"5an1ty","avatar_template":"/images/avatar.png"}},{"id":5542,"title":"Title character requirements not very visible","fancy_title":"Title character requirements not very visible","slug":"title-character-requirements-not-very-visible","posts_count":24,"reply_count":11,"highest_post_number":24,"image_url":null,"created_at":"2013-04-02T20:09:59.000-04:00","last_posted_at":"2014-01-15T05:26:07.000-05:00","bumped":true,"bumped_at":"2014-01-15T05:26:04.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"last_poster":{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"}}]},{"id":5,"name":"extensibility","color":"FE8432","text_color":"FFFFFF","slug":"extensibility","topic_count":102,"description":"Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ","topic_url":"/t/category-definition-for-extensibility/28","read_restricted":false,"permission":null,"post_count":964,"topics_day":0,"topics_week":2,"topics_month":18,"topics_year":102,"posts_day":0,"posts_week":17,"posts_month":76,"posts_year":964,"description_excerpt":"Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility.","featured_user_ids":[6548,32,8202,6677,7333],"topics":[{"id":11763,"title":"Google AdSense plugin is now available","fancy_title":"Google AdSense plugin is now available","slug":"google-adsense-plugin-is-now-available","posts_count":7,"reply_count":2,"highest_post_number":7,"image_url":"/uploads/default/_optimized/66d/cf0/d69e6709fe_496x500.PNG","created_at":"2014-01-05T14:28:58.000-05:00","last_posted_at":"2014-01-15T13:32:35.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:32:35.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":6548,"username":"michaeld","avatar_template":"/images/avatar.png"}},{"id":531,"title":"Discourse and Wordpress Integration","fancy_title":"Discourse and Wordpress Integration","slug":"discourse-and-wordpress-integration","posts_count":76,"reply_count":64,"highest_post_number":78,"image_url":null,"created_at":"2013-02-05T18:56:37.000-05:00","last_posted_at":"2014-01-15T11:56:54.000-05:00","bumped":true,"bumped_at":"2014-01-15T11:56:54.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}},{"id":11965,"title":"In your opinion, what is the best wiki engine to be associated with discourse?","fancy_title":"In your opinion, what is the best wiki engine to be associated with discourse?","slug":"in-your-opinion-what-is-the-best-wiki-engine-to-be-associated-with-discourse","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-14T19:27:06.000-05:00","last_posted_at":"2014-01-14T19:27:06.000-05:00","bumped":false,"bumped_at":"2014-01-14T19:27:06.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":8202,"username":"Matthieu","avatar_template":"/images/avatar.png"}}]},{"id":8,"name":"hosting","color":"74CCED","text_color":"FFFFFF","slug":"hosting","topic_count":69,"description":"Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services.","topic_url":"/t/category-definition-for-hosting/2626","read_restricted":false,"permission":null,"post_count":664,"topics_day":0,"topics_week":2,"topics_month":2,"topics_year":69,"posts_day":0,"posts_week":15,"posts_month":35,"posts_year":664,"description_excerpt":"Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services.","featured_user_ids":[6695,1,6018,1580,7030],"topics":[{"id":9540,"title":"Docker images for Discourse","fancy_title":"Docker images for Discourse","slug":"docker-images-for-discourse","posts_count":35,"reply_count":28,"highest_post_number":36,"image_url":null,"created_at":"2013-09-02T00:07:02.000-04:00","last_posted_at":"2014-01-16T07:47:18.000-05:00","bumped":true,"bumped_at":"2014-01-16T07:47:18.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":6695,"username":"illspirit","avatar_template":"/images/avatar.png"}},{"id":11971,"title":"Installing Discourse on Ubuntu 12.04 with Parallels Plesk and Apache","fancy_title":"Installing Discourse on Ubuntu 12.04 with Parallels Plesk and Apache","slug":"installing-discourse-on-ubuntu-12-04-with-parallels-plesk-and-apache","posts_count":3,"reply_count":1,"highest_post_number":3,"image_url":null,"created_at":"2014-01-15T04:23:38.000-05:00","last_posted_at":"2014-01-15T04:47:20.000-05:00","bumped":true,"bumped_at":"2014-01-15T04:47:20.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":7030,"username":"naabster","avatar_template":"/images/avatar.png"}},{"id":10844,"title":"Discourse in a Docker container","fancy_title":"Discourse in a Docker container","slug":"discourse-in-a-docker-container","posts_count":12,"reply_count":8,"highest_post_number":12,"image_url":null,"created_at":"2013-11-07T19:12:22.000-05:00","last_posted_at":"2014-01-11T14:43:53.000-05:00","bumped":true,"bumped_at":"2014-01-11T14:43:53.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":1,"username":"sam","avatar_template":"/images/avatar.png"}}]},{"id":17,"name":"uncategorized","color":"AB9364","text_color":"FFFFFF","slug":"uncategorized","topic_count":229,"description":"","topic_url":null,"read_restricted":false,"permission":null,"post_count":2138,"topics_day":0,"topics_week":0,"topics_month":9,"topics_year":229,"posts_day":1,"posts_week":11,"posts_month":183,"posts_year":2138,"description_excerpt":"","is_uncategorized":true,"featured_user_ids":[6973,32,1,1995,7073],"topics":[{"id":1,"title":"Welcome to meta.discourse.org","fancy_title":"Welcome to meta.discourse.org","slug":"welcome-to-meta-discourse-org","posts_count":5,"reply_count":5,"highest_post_number":23,"image_url":null,"created_at":"2013-01-31T23:52:28.000-05:00","last_posted_at":"2013-02-07T16:50:41.000-05:00","bumped":true,"bumped_at":"2013-02-07T11:57:34.000-05:00","unseen":false,"pinned":true,"excerpt":"Welcome to meta, the official site for discussing the next-gen open source Discourse forum software. You'll find topics on features, bugs, hosting, development, and general support here. \n\nDiscourse is early beta softwar…","visible":true,"closed":true,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}},{"id":11557,"title":"Error after upgrade to 0.9.7.9+","fancy_title":"Error after upgrade to 0.9.7.9+","slug":"error-after-upgrade-to-0-9-7-9","posts_count":83,"reply_count":58,"highest_post_number":85,"image_url":null,"created_at":"2013-12-22T17:12:05.000-05:00","last_posted_at":"2014-01-16T00:52:30.000-05:00","bumped":true,"bumped_at":"2014-01-16T00:52:30.000-05:00","unseen":false,"pinned":true,"excerpt":"Hi, \n\nI'm using webfaction postgresql specific private instance to run discourse (custom port already configured for discourse 0.9.7.6). \n\nThis is not my first update, but this time i have an error. Impossible to upgrade…","visible":true,"closed":false,"archived":false,"last_poster":{"id":6973,"username":"stellarhopper","avatar_template":"/images/avatar.png"}},{"id":6266,"title":"What sort of replies trigger a notice?","fancy_title":"What sort of replies trigger a notice?","slug":"what-sort-of-replies-trigger-a-notice","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2013-04-30T17:46:39.000-04:00","last_posted_at":"2014-01-16T00:52:21.000-05:00","bumped":true,"bumped_at":"2014-01-16T00:57:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}}]},{"id":11,"name":"login","color":"edb400","text_color":"FFFFFF","slug":"login","topic_count":27,"description":"Topics about logging in to Discourse, using any standard third party provider (Twitter, Facebook, Google), traditional username and password, or with a custom plugin.","topic_url":"/t/category-definition-for-login/2828","read_restricted":false,"permission":null,"post_count":200,"topics_day":0,"topics_week":1,"topics_month":1,"topics_year":27,"posts_day":0,"posts_week":10,"posts_month":27,"posts_year":200,"description_excerpt":"Topics about logging in to Discourse, using any standard third party provider (Twitter, Facebook, Google), traditional username and password, or with a custom plugin.","featured_user_ids":[8163,19,7796,32,8024],"topics":[{"id":11959,"title":"Get current user information via JSON","fancy_title":"Get current user information via JSON","slug":"get-current-user-information-via-json","posts_count":3,"reply_count":1,"highest_post_number":3,"image_url":null,"created_at":"2014-01-14T15:05:34.000-05:00","last_posted_at":"2014-01-14T16:43:28.000-05:00","bumped":true,"bumped_at":"2014-01-14T16:43:28.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":8163,"username":"znation","avatar_template":"/images/avatar.png"}},{"id":6242,"title":"Allow authentication via multiple services on one account","fancy_title":"Allow authentication via multiple services on one account","slug":"allow-authentication-via-multiple-services-on-one-account","posts_count":34,"reply_count":27,"highest_post_number":34,"image_url":null,"created_at":"2013-04-29T18:51:52.000-04:00","last_posted_at":"2014-01-14T00:25:42.000-05:00","bumped":true,"bumped_at":"2014-01-14T00:25:42.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":7796,"username":"almereyda","avatar_template":"/images/avatar.png"}},{"id":4738,"title":"Login support for browser password managers","fancy_title":"Login support for browser password managers","slug":"login-support-for-browser-password-managers","posts_count":6,"reply_count":2,"highest_post_number":6,"image_url":null,"created_at":"2013-03-13T17:55:29.000-04:00","last_posted_at":"2014-01-13T14:21:34.000-05:00","bumped":true,"bumped_at":"2014-01-13T14:21:34.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}}]},{"id":3,"name":"meta","color":"aaa","text_color":"FFFFFF","slug":"meta","topic_count":79,"description":"Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.","topic_url":"/t/category-definition-for-meta/24","read_restricted":false,"permission":null,"post_count":695,"topics_day":0,"topics_week":1,"topics_month":3,"topics_year":79,"posts_day":0,"posts_week":4,"posts_month":18,"posts_year":696,"description_excerpt":"Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.","featured_user_ids":[19,8085,32,5174,4534],"topics":[{"id":5249,"title":"What is \"Meta\"?","fancy_title":"What is “Meta”?","slug":"what-is-meta","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2013-03-25T18:00:52.000-04:00","last_posted_at":"2013-03-25T18:00:56.000-04:00","bumped":false,"bumped_at":"2013-03-25T18:00:52.000-04:00","unseen":false,"pinned":true,"excerpt":"Meta means discussion of the discussion itself instead of the actual topic of the discussion. \n\nWhy do we need a meta category?\n\nMeta is where communities come together to decide who they are and what they are about. \n…","visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}},{"id":11943,"title":"How far to take user documentation?","fancy_title":"How far to take user documentation?","slug":"how-far-to-take-user-documentation","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-13T19:21:26.000-05:00","last_posted_at":"2014-01-14T14:19:46.000-05:00","bumped":true,"bumped_at":"2014-01-14T14:19:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":19,"username":"eviltrout","avatar_template":"/images/avatar.png"}},{"id":11822,"title":"Search engine traffic share and level to Discourse","fancy_title":"Search engine traffic share and level to Discourse","slug":"search-engine-traffic-share-and-level-to-discourse","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2014-01-08T01:54:56.000-05:00","last_posted_at":"2014-01-08T02:21:25.000-05:00","bumped":true,"bumped_at":"2014-01-08T02:21:25.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}}]},{"id":12,"name":"discourse hub","color":"b2c79f","text_color":"FFFFFF","slug":"discourse-hub","topic_count":4,"description":"Topics about current or future Discourse Hub functionality at discourse.org including nickname registration, global user pages, and the site directory.","topic_url":"/t/category-definition-for-discourse-hub/3038","read_restricted":false,"permission":null,"post_count":121,"topics_day":0,"topics_week":0,"topics_month":0,"topics_year":4,"posts_day":0,"posts_week":3,"posts_month":3,"posts_year":121,"description_excerpt":"Topics about current or future Discourse Hub functionality at discourse.org including nickname registration, global user pages, and the site directory.","featured_user_ids":[2,32,2316,6695,4457],"topics":[{"id":6547,"title":"Where to get discourse_org_access_key?","fancy_title":"Where to get discourse_org_access_key?","slug":"where-to-get-discourse-org-access-key","posts_count":13,"reply_count":4,"highest_post_number":13,"image_url":null,"created_at":"2013-05-10T22:06:08.000-04:00","last_posted_at":"2014-01-13T11:38:15.000-05:00","bumped":true,"bumped_at":"2014-01-13T11:38:15.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":2,"username":"neil","avatar_template":"/images/avatar.png"}},{"id":2544,"title":"Discourse central hub questions","fancy_title":"Discourse central hub questions","slug":"discourse-central-hub-questions","posts_count":51,"reply_count":44,"highest_post_number":52,"image_url":null,"created_at":"2013-02-09T04:28:21.000-05:00","last_posted_at":"2013-09-19T13:36:49.000-04:00","bumped":true,"bumped_at":"2013-09-19T14:04:08.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":2128,"username":"ultimape","avatar_template":"/images/avatar.png"}},{"id":424,"title":"What are the 'consequences' of changing your name?","fancy_title":"What are the ‘consequences’ of changing your name?","slug":"what-are-the-consequences-of-changing-your-name","posts_count":35,"reply_count":36,"highest_post_number":43,"image_url":null,"created_at":"2013-02-05T17:37:52.000-05:00","last_posted_at":"2013-09-19T13:55:11.000-04:00","bumped":true,"bumped_at":"2013-09-19T13:55:11.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":2128,"username":"ultimape","avatar_template":"/images/avatar.png"}}]},{"id":13,"name":"blog","color":"ED207B","text_color":"FFFFFF","slug":"blog","topic_count":14,"description":"Discussion topics generated from the official Discourse Blog. These topics are linked from the bottom of each blog entry where the blog comments would normally be.","topic_url":"/t/category-definition-for-blog/5250","read_restricted":false,"permission":null,"post_count":206,"topics_day":0,"topics_week":0,"topics_month":1,"topics_year":14,"posts_day":0,"posts_week":2,"posts_month":11,"posts_year":206,"description_excerpt":"Discussion topics generated from the official Discourse Blog. These topics are linked from the bottom of each blog entry where the blog comments would normally be.","featured_user_ids":[8134,32,4457,4263,1995],"topics":[{"id":11835,"title":"The Road to Discourse 1.0","fancy_title":"The Road to Discourse 1.0","slug":"the-road-to-discourse-1-0","posts_count":6,"reply_count":2,"highest_post_number":6,"image_url":null,"created_at":"2014-01-08T19:08:44.000-05:00","last_posted_at":"2014-01-16T04:49:16.000-05:00","bumped":true,"bumped_at":"2014-01-16T04:49:16.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":8134,"username":"iontishina","avatar_template":"/images/avatar.png"}},{"id":5751,"title":"Discourse as Your First Rails App","fancy_title":"Discourse as Your First Rails App","slug":"discourse-as-your-first-rails-app","posts_count":62,"reply_count":43,"highest_post_number":71,"image_url":null,"created_at":"2013-04-09T19:08:33.000-04:00","last_posted_at":"2013-12-19T18:27:37.000-05:00","bumped":true,"bumped_at":"2013-12-19T18:27:37.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"}},{"id":5898,"title":"The Discourse Servers","fancy_title":"The Discourse Servers","slug":"the-discourse-servers","posts_count":42,"reply_count":32,"highest_post_number":42,"image_url":null,"created_at":"2013-04-15T15:19:09.000-04:00","last_posted_at":"2013-11-29T15:14:35.000-05:00","bumped":true,"bumped_at":"2013-11-29T15:14:35.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":6626,"username":"riking","avatar_template":"/images/avatar.png"}}]},{"id":4,"name":"faq","color":"33b","text_color":"FFFFFF","slug":"faq","topic_count":49,"description":"Topics that come up very often when discussing Discourse will eventually be classified into this Frequently Asked Questions category. Should only be added to popular topics.","topic_url":"/t/category-definition-for-faq/25","read_restricted":false,"permission":null,"post_count":450,"topics_day":0,"topics_week":0,"topics_month":0,"topics_year":49,"posts_day":0,"posts_week":1,"posts_month":10,"posts_year":450,"description_excerpt":"Topics that come up very often when discussing Discourse will eventually be classified into this Frequently Asked Questions category. Should only be added to popular topics.","featured_user_ids":[32,8047,7483,2,6626],"topics":[{"id":5372,"title":"UX confusion (or me confusion) is it possible to edit old posts or only your most recent post in a topic?","fancy_title":"UX confusion (or me confusion) is it possible to edit old posts or only your most recent post in a topic?","slug":"ux-confusion-or-me-confusion-is-it-possible-to-edit-old-posts-or-only-your-most-recent-post-in-a-topic","posts_count":3,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2013-03-28T22:25:57.000-04:00","last_posted_at":"2014-01-13T13:44:39.000-05:00","bumped":true,"bumped_at":"2014-01-13T13:44:39.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}},{"id":9631,"title":"All the options to deploy Discourse with their relative pros and cons","fancy_title":"All the options to deploy Discourse with their relative pros and cons","slug":"all-the-options-to-deploy-discourse-with-their-relative-pros-and-cons","posts_count":14,"reply_count":7,"highest_post_number":15,"image_url":null,"created_at":"2013-09-06T03:55:09.000-04:00","last_posted_at":"2013-09-26T18:49:04.000-04:00","bumped":true,"bumped_at":"2013-12-30T12:32:59.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":3929,"username":"ScotterC","avatar_template":"/images/avatar.png"}},{"id":4325,"title":"How to delete a user?","fancy_title":"How to delete a user?","slug":"how-to-delete-a-user","posts_count":31,"reply_count":23,"highest_post_number":33,"image_url":null,"created_at":"2013-03-01T23:18:55.000-05:00","last_posted_at":"2013-12-20T21:26:06.000-05:00","bumped":true,"bumped_at":"2013-12-20T21:26:06.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}}]},{"id":14,"name":"marketplace","color":"8C6238","text_color":"FFFFFF","slug":"marketplace","topic_count":24,"description":"About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc.","topic_url":"/t/category-definition-for-marketplace/5425","read_restricted":false,"permission":null,"post_count":106,"topics_day":0,"topics_week":1,"topics_month":3,"topics_year":24,"posts_day":0,"posts_week":1,"posts_month":7,"posts_year":106,"description_excerpt":"About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc.","featured_user_ids":[6548,32,5548,2291,4755],"topics":[{"id":11866,"title":"DiscourseHosting is now accepting BTC payments","fancy_title":"DiscourseHosting is now accepting BTC payments","slug":"discoursehosting-is-now-accepting-btc-payments","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-10T10:17:28.000-05:00","last_posted_at":"2014-01-10T10:17:28.000-05:00","bumped":false,"bumped_at":"2014-01-10T10:17:28.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":6548,"username":"michaeld","avatar_template":"/images/avatar.png"}},{"id":11571,"title":"Looking for a developer for Discourse Customization","fancy_title":"Looking for a developer for Discourse Customization","slug":"looking-for-a-developer-for-discourse-customization","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2013-12-23T20:54:04.000-05:00","last_posted_at":"2013-12-24T13:12:17.000-05:00","bumped":true,"bumped_at":"2013-12-30T16:36:17.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":2291,"username":"PabloC","avatar_template":"/images/avatar.png"}},{"id":11594,"title":"Need someone to fix a topic in my discourse install that won't load for moderators. Will pay","fancy_title":"Need someone to fix a topic in my discourse install that won’t load for moderators. Will pay","slug":"need-someone-to-fix-a-topic-in-my-discourse-install-that-wont-load-for-moderators-will-pay","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2013-12-25T10:25:57.000-05:00","last_posted_at":"2013-12-26T17:01:41.000-05:00","bumped":true,"bumped_at":"2013-12-25T17:01:15.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"last_poster":{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"}}]},{"id":10,"name":"howto","color":"76923C","text_color":"FFFFFF","slug":"howto","topic_count":58,"description":"Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up. ","topic_url":"/t/category-definition-for-howto/2629","read_restricted":false,"permission":null,"post_count":677,"topics_day":0,"topics_week":0,"topics_month":1,"topics_year":58,"posts_day":0,"posts_week":0,"posts_month":13,"posts_year":675,"description_excerpt":"Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up.","featured_user_ids":[7984,4457,1995,6018,5351],"topics":[{"id":7582,"title":"Twitter login with Passenger + Varnish - quick lessons learned","fancy_title":"Twitter login with Passenger + Varnish - quick lessons learned","slug":"twitter-login-with-passenger-varnish-quick-lessons-learned","posts_count":9,"reply_count":3,"highest_post_number":9,"image_url":"/plugins/emoji/images/smile.png","created_at":"2013-06-17T19:46:31.000-04:00","last_posted_at":"2013-12-31T21:03:59.000-05:00","bumped":true,"bumped_at":"2013-12-31T21:03:59.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":7984,"username":"sophearak","avatar_template":"/images/avatar.png"}},{"id":7229,"title":"How to set up image uploads to S3?","fancy_title":"How to set up image uploads to S3?","slug":"how-to-set-up-image-uploads-to-s3","posts_count":14,"reply_count":11,"highest_post_number":14,"image_url":"/uploads/meta_discourse/1019/782cbc7e309ce43f.png","created_at":"2013-06-06T15:37:43.000-04:00","last_posted_at":"2013-12-31T11:54:18.000-05:00","bumped":true,"bumped_at":"2013-12-31T11:54:18.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"}},{"id":11628,"title":"My experience with a successful migration (hints for a guide)","fancy_title":"My experience with a successful migration (hints for a guide)","slug":"my-experience-with-a-successful-migration-hints-for-a-guide","posts_count":3,"reply_count":1,"highest_post_number":3,"image_url":null,"created_at":"2013-12-28T09:23:45.000-05:00","last_posted_at":"2013-12-28T10:38:48.000-05:00","bumped":true,"bumped_at":"2013-12-28T10:38:48.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"last_poster":{"id":6018,"username":"robypez","avatar_template":"/images/avatar.png"}}]}]}}, -"/c/bug/l/latest.json": {"users":[{"id":1,"username":"sam","avatar_template":"/images/avatar.png"},{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"},{"id":8021,"username":"Abhishek_Gupta","avatar_template":"/images/avatar.png"},{"id":6695,"username":"illspirit","avatar_template":"/images/avatar.png"},{"id":2,"username":"neil","avatar_template":"/images/avatar.png"},{"id":3124,"username":"sipp11","avatar_template":"/images/avatar.png"},{"id":7513,"username":"digit","avatar_template":"/images/avatar.png"},{"id":19,"username":"eviltrout","avatar_template":"/images/avatar.png"},{"id":3,"username":"supermathie","avatar_template":"/images/avatar.png"},{"id":7073,"username":"5an1ty","avatar_template":"/images/avatar.png"},{"id":4996,"username":"wmertens","avatar_template":"/images/avatar.png"},{"id":6377,"username":"zh99998","avatar_template":"/images/avatar.png"},{"id":1496,"username":"cfstras","avatar_template":"/images/avatar.png"},{"id":7995,"username":"Hunter","avatar_template":"/images/avatar.png"},{"id":6626,"username":"riking","avatar_template":"/images/avatar.png"},{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"},{"id":5048,"username":"SneakySly","avatar_template":"/images/avatar.png"},{"id":7731,"username":"YOU","avatar_template":"/images/avatar.png"},{"id":7985,"username":"onlinedev","avatar_template":"/images/avatar.png"},{"id":3415,"username":"radq","avatar_template":"/images/avatar.png"},{"id":5351,"username":"erlend_sh","avatar_template":"/images/avatar.png"},{"id":471,"username":"BhaelOchon","avatar_template":"/images/avatar.png"},{"id":7,"username":"pekka","avatar_template":"/images/avatar.png"},{"id":4780,"username":"HugoAlmeida","avatar_template":"/images/avatar.png"},{"id":5053,"username":"Blue","avatar_template":"/images/avatar.png"},{"id":212,"username":"alxndr","avatar_template":"/images/avatar.png"},{"id":6118,"username":"lukelarris","avatar_template":"/images/avatar.png"},{"id":7076,"username":"philnelson","avatar_template":"/images/avatar.png"},{"id":4851,"username":"jab","avatar_template":"/images/avatar.png"},{"id":4457,"username":"Lee_Ars","avatar_template":"/images/avatar.png"},{"id":6280,"username":"mx2000","avatar_template":"/images/avatar.png"},{"id":3681,"username":"Ajarn","avatar_template":"/images/avatar.png"},{"id":1621,"username":"bnb","avatar_template":"/images/avatar.png"},{"id":6266,"username":"bragi","avatar_template":"/images/avatar.png"},{"id":5335,"username":"masda70","avatar_template":"/images/avatar.png"},{"id":6314,"username":"rafaelfranca","avatar_template":"/images/avatar.png"}],"topic_list":{"can_create_topic":false,"more_topics_url":"/latest.json?category=1&page=1","draft":null,"draft_key":"new_topic","draft_sequence":null,"topics":[{"id":2,"title":"Category definition for bug","fancy_title":"Category definition for bug","slug":"category-definition-for-bug","posts_count":2,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2013-01-31T23:56:34.000-05:00","last_posted_at":"2013-03-07T22:42:27.000-05:00","bumped":true,"bumped_at":"2013-02-26T18:52:56.000-05:00","unseen":false,"pinned":true,"excerpt":"Bug reports on Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","visible":true,"closed":false,"archived":false,"views":469,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11994,"title":"Cross domain rules, followed?","fancy_title":"Cross domain rules, followed?","slug":"cross-domain-rules-followed","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-16T09:59:15.000-05:00","last_posted_at":"2014-01-16T09:59:15.000-05:00","bumped":true,"bumped_at":"2014-01-16T11:04:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":15,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Abhishek_Gupta","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":8021}]},{"id":11888,"title":"Uncategorized topics not allowed, still seeing tag places","fancy_title":"Uncategorized topics not allowed, still seeing tag places","slug":"uncategorized-topics-not-allowed-still-seeing-tag-places","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2014-01-10T19:23:37.000-05:00","last_posted_at":"2014-01-15T22:41:25.000-05:00","bumped":true,"bumped_at":"2014-01-15T22:41:25.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":50,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"illspirit","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6695},{"extras":null,"description":"Most Posts","user_id":32},{"extras":null,"description":"Frequent Poster","user_id":2}]},{"id":9151,"title":"Apple touch icon doesn't show if there is no sub domain","fancy_title":"Apple touch icon doesn’t show if there is no sub domain","slug":"apple-touch-icon-doesnt-show-if-there-is-no-sub-domain","posts_count":7,"reply_count":4,"highest_post_number":7,"image_url":null,"created_at":"2013-08-16T18:16:53.000-04:00","last_posted_at":"2014-01-15T17:10:18.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:19:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":188,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":3124},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":10911,"title":"/users/activate-account pulling blank logo instead of defaulting to h2","fancy_title":"/users/activate-account pulling blank logo instead of defaulting to h2","slug":"users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2","posts_count":3,"reply_count":1,"highest_post_number":3,"image_url":null,"created_at":"2013-11-12T14:49:04.000-05:00","last_posted_at":"2014-01-15T10:21:37.000-05:00","bumped":true,"bumped_at":"2014-01-15T10:21:37.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":121,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":7513},{"extras":"latest","description":"Most Recent Poster","user_id":19}]},{"id":11937,"title":"Smiley parser is busted","fancy_title":"Smiley parser is busted","slug":"smiley-parser-is-busted","posts_count":4,"reply_count":4,"highest_post_number":7,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-13T15:42:00.000-05:00","last_posted_at":"2014-01-15T05:51:16.000-05:00","bumped":true,"bumped_at":"2014-01-15T05:51:16.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":66,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":3},{"extras":null,"description":"Most Posts","user_id":7073},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":6625,"title":"Error 500 on PUT of site config","fancy_title":"Error 500 on PUT of site config","slug":"error-500-on-put-of-site-config","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2013-05-14T18:13:56.000-04:00","last_posted_at":"2014-01-16T04:55:50.000-05:00","bumped":true,"bumped_at":"2014-01-15T04:43:23.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":132,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":4996},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11225,"title":"Forum acts weirdly after client side updates","fancy_title":"Forum acts weirdly after client side updates","slug":"forum-acts-weirdly-after-client-side-updates","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2013-12-02T18:32:10.000-05:00","last_posted_at":"2014-01-15T04:04:55.000-05:00","bumped":true,"bumped_at":"2014-01-15T02:55:18.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":117,"like_count":7,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11903,"title":"Error after update to 0.9.8.1","fancy_title":"Error after update to 0.9.8.1","slug":"error-after-update-to-0-9-8-1","posts_count":14,"reply_count":6,"highest_post_number":17,"image_url":null,"created_at":"2014-01-12T06:55:45.000-05:00","last_posted_at":"2014-01-15T01:48:58.000-05:00","bumped":true,"bumped_at":"2014-01-15T01:48:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":121,"like_count":6,"has_summary":false,"archetype":"regular","last_poster_username":"zh99998","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6377},{"extras":null,"description":"Most Posts","user_id":1496},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":19}]},{"id":11969,"title":"Qunit error and possibly related ember.js problem","fancy_title":"Qunit error and possibly related ember.js problem","slug":"qunit-error-and-possibly-related-ember-js-problem","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-14T22:51:32.000-05:00","last_posted_at":"2014-01-14T22:51:32.000-05:00","bumped":false,"bumped_at":"2014-01-14T22:51:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":32,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Hunter","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":7995}]},{"id":11945,"title":"Stuff disappears on the groups page","fancy_title":"Stuff disappears on the groups page","slug":"stuff-disappears-on-the-groups-page","posts_count":7,"reply_count":2,"highest_post_number":7,"image_url":null,"created_at":"2014-01-13T23:03:53.000-05:00","last_posted_at":"2014-01-15T01:26:07.000-05:00","bumped":true,"bumped_at":"2014-01-14T21:09:01.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":54,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":6695},{"extras":null,"description":"Most Posts","user_id":6626},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1995}]},{"id":11520,"title":"Discourse WordPress Plugin: Emoji's do not properly display","fancy_title":"Discourse WordPress Plugin: Emoji’s do not properly display","slug":"discourse-wordpress-plugin-emojis-do-not-properly-display","posts_count":9,"reply_count":4,"highest_post_number":9,"image_url":"/uploads/default/_optimized/638/4db/eff43a45b8_690x420.png","created_at":"2013-12-19T23:32:03.000-05:00","last_posted_at":"2014-01-15T04:32:19.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:53:34.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":168,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":5048},{"extras":null,"description":"Frequent Poster","user_id":7731},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11597,"title":"All categories drop down does not close after clicking on first menu \"all categories\"","fancy_title":"All categories drop down does not close after clicking on first menu “all categories”","slug":"all-categories-drop-down-does-not-close-after-clicking-on-first-menu-all-categories","posts_count":5,"reply_count":2,"highest_post_number":5,"image_url":"/uploads/default/2495/f9efe463ae67632d.png","created_at":"2013-12-25T15:09:27.000-05:00","last_posted_at":"2014-01-14T17:46:41.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:46:41.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":73,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"radq","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":7985},{"extras":null,"description":"Most Posts","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":3415}]},{"id":11962,"title":"Editor When Clicking on Wrench Issue","fancy_title":"Editor When Clicking on Wrench Issue","slug":"editor-when-clicking-on-wrench-issue","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/_optimized/ca4/f70/ac7278b8f6_690x176.png","created_at":"2014-01-14T17:23:20.000-05:00","last_posted_at":"2014-01-14T17:24:02.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:24:02.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":30,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":7073},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11831,"title":"Broken links, possibly related to HTTPS","fancy_title":"Broken links, possibly related to HTTPS","slug":"broken-links-possibly-related-to-https","posts_count":17,"reply_count":13,"highest_post_number":18,"image_url":null,"created_at":"2014-01-08T17:40:45.000-05:00","last_posted_at":"2014-01-14T16:03:07.000-05:00","bumped":true,"bumped_at":"2014-01-14T16:03:07.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":102,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":5351},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":471},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":19}]},{"id":11916,"title":"Unable to save user preferences","fancy_title":"Unable to save user preferences","slug":"unable-to-save-user-preferences","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2014-01-13T02:29:26.000-05:00","last_posted_at":"2014-01-14T14:39:32.000-05:00","bumped":true,"bumped_at":"2014-01-14T14:39:29.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":34,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":10425,"title":"Editing category permissions: select value doesn't change","fancy_title":"Editing category permissions: select value doesn’t change","slug":"editing-category-permissions-select-value-doesnt-change","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/uploads/meta_discourse/1956/d55fba29dbd7e1fe.png","created_at":"2013-10-17T18:20:20.000-04:00","last_posted_at":"2013-10-17T18:20:21.000-04:00","bumped":true,"bumped_at":"2014-01-14T13:35:37.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":92,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"pekka","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":7}]},{"id":6557,"title":"Middle clicking a link twice does not work as expected","fancy_title":"Middle clicking a link twice does not work as expected","slug":"middle-clicking-a-link-twice-does-not-work-as-expected","posts_count":10,"reply_count":7,"highest_post_number":10,"image_url":null,"created_at":"2013-05-11T13:56:02.000-04:00","last_posted_at":"2014-01-14T13:13:04.000-05:00","bumped":true,"bumped_at":"2014-01-14T13:13:04.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":401,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"neil","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":4780},{"extras":null,"description":"Most Posts","user_id":5053},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":2}]},{"id":11944,"title":"Regression: Cannot sort topic list","fancy_title":"Regression: Cannot sort topic list","slug":"regression-cannot-sort-topic-list","posts_count":5,"reply_count":0,"highest_post_number":5,"image_url":null,"created_at":"2014-01-13T20:14:06.000-05:00","last_posted_at":"2014-01-14T19:31:28.000-05:00","bumped":true,"bumped_at":"2014-01-14T07:31:19.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":true,"views":37,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":1995}]},{"id":10462,"title":"Rebake error when posts contain deleted YouTube video","fancy_title":"Rebake error when posts contain deleted YouTube video","slug":"rebake-error-when-posts-contain-deleted-youtube-video","posts_count":7,"reply_count":1,"highest_post_number":7,"image_url":null,"created_at":"2013-10-19T00:01:21.000-04:00","last_posted_at":"2014-01-14T02:24:19.000-05:00","bumped":true,"bumped_at":"2014-01-14T02:24:12.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":178,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":6695},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11932,"title":"Use of blockquote tag causes text outside a paragraph","fancy_title":"Use of blockquote tag causes text outside a paragraph","slug":"use-of-blockquote-tag-causes-text-outside-a-paragraph","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":null,"created_at":"2014-01-13T13:38:15.000-05:00","last_posted_at":"2014-01-13T19:30:37.000-05:00","bumped":true,"bumped_at":"2014-01-14T02:22:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":54,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":10357,"title":"Displaced Wrench Icon Chrome","fancy_title":"Displaced Wrench Icon Chrome","slug":"displaced-wrench-icon-chrome","posts_count":12,"reply_count":4,"highest_post_number":12,"image_url":"/uploads/default/_optimized/9f3/f35/c5379beffe_690x300.jpg","created_at":"2013-10-14T05:48:21.000-04:00","last_posted_at":"2014-01-14T03:21:32.000-05:00","bumped":true,"bumped_at":"2014-01-13T19:03:33.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":206,"like_count":10,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":7073},{"extras":null,"description":"Frequent Poster","user_id":212},{"extras":null,"description":"Frequent Poster","user_id":6118},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":10114,"title":"Invitation expiry workflow is wonky","fancy_title":"Invitation expiry workflow is wonky","slug":"invitation-expiry-workflow-is-wonky","posts_count":14,"reply_count":7,"highest_post_number":14,"image_url":null,"created_at":"2013-09-30T00:59:36.000-04:00","last_posted_at":"2014-01-13T18:51:26.000-05:00","bumped":true,"bumped_at":"2014-01-13T18:51:26.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":176,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":null,"description":"Most Posts","user_id":7076},{"extras":null,"description":"Frequent Poster","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":6330,"title":"Reply not disabled if topic closed while viewing","fancy_title":"Reply not disabled if topic closed while viewing","slug":"reply-not-disabled-if-topic-closed-while-viewing","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon","created_at":"2013-05-02T06:02:06.000-04:00","last_posted_at":"2014-01-13T11:54:22.000-05:00","bumped":true,"bumped_at":"2014-01-13T11:54:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":164,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":4851},{"extras":null,"description":"Most Posts","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":8367,"title":"Very fast scrolling fails to mark all posts read in a thread","fancy_title":"Very fast scrolling fails to mark all posts read in a thread","slug":"very-fast-scrolling-fails-to-mark-all-posts-read-in-a-thread","posts_count":11,"reply_count":7,"highest_post_number":13,"image_url":null,"created_at":"2013-07-14T12:37:02.000-04:00","last_posted_at":"2014-01-13T11:16:56.000-05:00","bumped":true,"bumped_at":"2014-01-13T11:16:33.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":288,"like_count":5,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":4457},{"extras":null,"description":"Most Posts","user_id":6280},{"extras":null,"description":"Frequent Poster","user_id":3681},{"extras":null,"description":"Frequent Poster","user_id":1621},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":8815,"title":"Cache headers confuse proxies","fancy_title":"Cache headers confuse proxies","slug":"cache-headers-confuse-proxies","posts_count":9,"reply_count":3,"highest_post_number":9,"image_url":null,"created_at":"2013-08-02T05:45:26.000-04:00","last_posted_at":"2014-01-13T11:12:09.000-05:00","bumped":true,"bumped_at":"2014-01-13T10:41:44.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":314,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":6266},{"extras":null,"description":"Most Posts","user_id":19},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":4457},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":32}]},{"id":11371,"title":"Search not working for Staff users","fancy_title":"Search not working for Staff users","slug":"search-not-working-for-staff-users","posts_count":15,"reply_count":10,"highest_post_number":15,"image_url":null,"created_at":"2013-12-11T13:22:56.000-05:00","last_posted_at":"2014-01-13T01:41:50.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:41:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":217,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":5335},{"extras":null,"description":"Most Posts","user_id":19},{"extras":null,"description":"Frequent Poster","user_id":6314},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":9908,"title":"Draft bar overrides pagination widget","fancy_title":"Draft bar overrides pagination widget","slug":"draft-bar-overrides-pagination-widget","posts_count":4,"reply_count":0,"highest_post_number":4,"image_url":null,"created_at":"2013-09-19T17:19:52.000-04:00","last_posted_at":"2014-01-13T01:26:01.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:25:12.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":108,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":5351},{"extras":null,"description":"Most Posts","user_id":471},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":6134,"title":"Unread topic is stuck as unread after insertion of staff message","fancy_title":"Unread topic is stuck as unread after insertion of staff message","slug":"unread-topic-is-stuck-as-unread-after-insertion-of-staff-message","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon","created_at":"2013-04-24T13:37:32.000-04:00","last_posted_at":"2014-01-13T01:22:49.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:22:42.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":169,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":1,"posters":[{"extras":null,"description":"Original Poster","user_id":3681},{"extras":null,"description":"Most Posts","user_id":5351},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11914,"title":"Google analytics is not registering page views","fancy_title":"Google analytics is not registering page views","slug":"google-analytics-is-not-registering-page-views","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-13T00:32:45.000-05:00","last_posted_at":"2014-01-13T00:32:46.000-05:00","bumped":true,"bumped_at":"2014-01-13T00:32:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":37,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":1,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":1}]}]}}, -"/c/feature/l/latest.json": {"users":[{"id":1,"username":"sam","avatar_template":"/images/avatar.png"},{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"},{"id":8021,"username":"Abhishek_Gupta","avatar_template":"/images/avatar.png"},{"id":6695,"username":"illspirit","avatar_template":"/images/avatar.png"},{"id":2,"username":"neil","avatar_template":"/images/avatar.png"},{"id":3124,"username":"sipp11","avatar_template":"/images/avatar.png"},{"id":7513,"username":"digit","avatar_template":"/images/avatar.png"},{"id":19,"username":"eviltrout","avatar_template":"/images/avatar.png"},{"id":3,"username":"supermathie","avatar_template":"/images/avatar.png"},{"id":7073,"username":"5an1ty","avatar_template":"/images/avatar.png"},{"id":4996,"username":"wmertens","avatar_template":"/images/avatar.png"},{"id":6377,"username":"zh99998","avatar_template":"/images/avatar.png"},{"id":1496,"username":"cfstras","avatar_template":"/images/avatar.png"},{"id":7995,"username":"Hunter","avatar_template":"/images/avatar.png"},{"id":6626,"username":"riking","avatar_template":"/images/avatar.png"},{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"},{"id":5048,"username":"SneakySly","avatar_template":"/images/avatar.png"},{"id":7731,"username":"YOU","avatar_template":"/images/avatar.png"},{"id":7985,"username":"onlinedev","avatar_template":"/images/avatar.png"},{"id":3415,"username":"radq","avatar_template":"/images/avatar.png"},{"id":5351,"username":"erlend_sh","avatar_template":"/images/avatar.png"},{"id":471,"username":"BhaelOchon","avatar_template":"/images/avatar.png"},{"id":7,"username":"pekka","avatar_template":"/images/avatar.png"},{"id":4780,"username":"HugoAlmeida","avatar_template":"/images/avatar.png"},{"id":5053,"username":"Blue","avatar_template":"/images/avatar.png"},{"id":212,"username":"alxndr","avatar_template":"/images/avatar.png"},{"id":6118,"username":"lukelarris","avatar_template":"/images/avatar.png"},{"id":7076,"username":"philnelson","avatar_template":"/images/avatar.png"},{"id":4851,"username":"jab","avatar_template":"/images/avatar.png"},{"id":4457,"username":"Lee_Ars","avatar_template":"/images/avatar.png"},{"id":6280,"username":"mx2000","avatar_template":"/images/avatar.png"},{"id":3681,"username":"Ajarn","avatar_template":"/images/avatar.png"},{"id":1621,"username":"bnb","avatar_template":"/images/avatar.png"},{"id":6266,"username":"bragi","avatar_template":"/images/avatar.png"},{"id":5335,"username":"masda70","avatar_template":"/images/avatar.png"},{"id":6314,"username":"rafaelfranca","avatar_template":"/images/avatar.png"}],"topic_list":{"can_create_topic":false,"more_topics_url":"/latest.json?category=2&page=1","draft":null,"draft_key":"new_topic","draft_sequence":null,"topics":[{"id":2,"title":"Category definition for feature","fancy_title":"Category definition for feature","slug":"category-definition-for-feature","posts_count":2,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2013-01-31T23:56:34.000-05:00","last_posted_at":"2013-03-07T22:42:27.000-05:00","bumped":true,"bumped_at":"2013-02-26T18:52:56.000-05:00","unseen":false,"pinned":true,"excerpt":"Features on Discourse.","visible":true,"closed":false,"archived":false,"views":469,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11994,"title":"Cross domain rules, followed?","fancy_title":"Cross domain rules, followed?","slug":"cross-domain-rules-followed","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-16T09:59:15.000-05:00","last_posted_at":"2014-01-16T09:59:15.000-05:00","bumped":true,"bumped_at":"2014-01-16T11:04:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":15,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Abhishek_Gupta","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":8021}]},{"id":11888,"title":"Uncategorized topics not allowed, still seeing tag places","fancy_title":"Uncategorized topics not allowed, still seeing tag places","slug":"uncategorized-topics-not-allowed-still-seeing-tag-places","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2014-01-10T19:23:37.000-05:00","last_posted_at":"2014-01-15T22:41:25.000-05:00","bumped":true,"bumped_at":"2014-01-15T22:41:25.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":50,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"illspirit","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6695},{"extras":null,"description":"Most Posts","user_id":32},{"extras":null,"description":"Frequent Poster","user_id":2}]},{"id":9151,"title":"Apple touch icon doesn't show if there is no sub domain","fancy_title":"Apple touch icon doesn’t show if there is no sub domain","slug":"apple-touch-icon-doesnt-show-if-there-is-no-sub-domain","posts_count":7,"reply_count":4,"highest_post_number":7,"image_url":null,"created_at":"2013-08-16T18:16:53.000-04:00","last_posted_at":"2014-01-15T17:10:18.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:19:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":188,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":3124},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":10911,"title":"/users/activate-account pulling blank logo instead of defaulting to h2","fancy_title":"/users/activate-account pulling blank logo instead of defaulting to h2","slug":"users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2","posts_count":3,"reply_count":1,"highest_post_number":3,"image_url":null,"created_at":"2013-11-12T14:49:04.000-05:00","last_posted_at":"2014-01-15T10:21:37.000-05:00","bumped":true,"bumped_at":"2014-01-15T10:21:37.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":121,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":7513},{"extras":"latest","description":"Most Recent Poster","user_id":19}]},{"id":11937,"title":"Smiley parser is busted","fancy_title":"Smiley parser is busted","slug":"smiley-parser-is-busted","posts_count":4,"reply_count":4,"highest_post_number":7,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-13T15:42:00.000-05:00","last_posted_at":"2014-01-15T05:51:16.000-05:00","bumped":true,"bumped_at":"2014-01-15T05:51:16.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":66,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":3},{"extras":null,"description":"Most Posts","user_id":7073},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":6625,"title":"Error 500 on PUT of site config","fancy_title":"Error 500 on PUT of site config","slug":"error-500-on-put-of-site-config","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2013-05-14T18:13:56.000-04:00","last_posted_at":"2014-01-16T04:55:50.000-05:00","bumped":true,"bumped_at":"2014-01-15T04:43:23.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":132,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":4996},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11225,"title":"Forum acts weirdly after client side updates","fancy_title":"Forum acts weirdly after client side updates","slug":"forum-acts-weirdly-after-client-side-updates","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2013-12-02T18:32:10.000-05:00","last_posted_at":"2014-01-15T04:04:55.000-05:00","bumped":true,"bumped_at":"2014-01-15T02:55:18.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":117,"like_count":7,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11903,"title":"Error after update to 0.9.8.1","fancy_title":"Error after update to 0.9.8.1","slug":"error-after-update-to-0-9-8-1","posts_count":14,"reply_count":6,"highest_post_number":17,"image_url":null,"created_at":"2014-01-12T06:55:45.000-05:00","last_posted_at":"2014-01-15T01:48:58.000-05:00","bumped":true,"bumped_at":"2014-01-15T01:48:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":121,"like_count":6,"has_summary":false,"archetype":"regular","last_poster_username":"zh99998","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6377},{"extras":null,"description":"Most Posts","user_id":1496},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":19}]},{"id":11969,"title":"Qunit error and possibly related ember.js problem","fancy_title":"Qunit error and possibly related ember.js problem","slug":"qunit-error-and-possibly-related-ember-js-problem","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-14T22:51:32.000-05:00","last_posted_at":"2014-01-14T22:51:32.000-05:00","bumped":false,"bumped_at":"2014-01-14T22:51:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":32,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Hunter","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":7995}]},{"id":11945,"title":"Stuff disappears on the groups page","fancy_title":"Stuff disappears on the groups page","slug":"stuff-disappears-on-the-groups-page","posts_count":7,"reply_count":2,"highest_post_number":7,"image_url":null,"created_at":"2014-01-13T23:03:53.000-05:00","last_posted_at":"2014-01-15T01:26:07.000-05:00","bumped":true,"bumped_at":"2014-01-14T21:09:01.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":54,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6695},{"extras":null,"description":"Most Posts","user_id":6626},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1995}]},{"id":11520,"title":"Discourse WordPress Plugin: Emoji's do not properly display","fancy_title":"Discourse WordPress Plugin: Emoji’s do not properly display","slug":"discourse-wordpress-plugin-emojis-do-not-properly-display","posts_count":9,"reply_count":4,"highest_post_number":9,"image_url":"/uploads/default/_optimized/638/4db/eff43a45b8_690x420.png","created_at":"2013-12-19T23:32:03.000-05:00","last_posted_at":"2014-01-15T04:32:19.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:53:34.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":168,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":5048},{"extras":null,"description":"Frequent Poster","user_id":7731},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11597,"title":"All categories drop down does not close after clicking on first menu \"all categories\"","fancy_title":"All categories drop down does not close after clicking on first menu “all categories”","slug":"all-categories-drop-down-does-not-close-after-clicking-on-first-menu-all-categories","posts_count":5,"reply_count":2,"highest_post_number":5,"image_url":"/uploads/default/2495/f9efe463ae67632d.png","created_at":"2013-12-25T15:09:27.000-05:00","last_posted_at":"2014-01-14T17:46:41.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:46:41.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":73,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"radq","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":7985},{"extras":null,"description":"Most Posts","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":3415}]},{"id":11962,"title":"Editor When Clicking on Wrench Issue","fancy_title":"Editor When Clicking on Wrench Issue","slug":"editor-when-clicking-on-wrench-issue","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/_optimized/ca4/f70/ac7278b8f6_690x176.png","created_at":"2014-01-14T17:23:20.000-05:00","last_posted_at":"2014-01-14T17:24:02.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:24:02.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":30,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":7073},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11831,"title":"Broken links, possibly related to HTTPS","fancy_title":"Broken links, possibly related to HTTPS","slug":"broken-links-possibly-related-to-https","posts_count":17,"reply_count":13,"highest_post_number":18,"image_url":null,"created_at":"2014-01-08T17:40:45.000-05:00","last_posted_at":"2014-01-14T16:03:07.000-05:00","bumped":true,"bumped_at":"2014-01-14T16:03:07.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":102,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":5351},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":471},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":19}]},{"id":11916,"title":"Unable to save user preferences","fancy_title":"Unable to save user preferences","slug":"unable-to-save-user-preferences","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2014-01-13T02:29:26.000-05:00","last_posted_at":"2014-01-14T14:39:32.000-05:00","bumped":true,"bumped_at":"2014-01-14T14:39:29.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":34,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":10425,"title":"Editing category permissions: select value doesn't change","fancy_title":"Editing category permissions: select value doesn’t change","slug":"editing-category-permissions-select-value-doesnt-change","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/uploads/meta_discourse/1956/d55fba29dbd7e1fe.png","created_at":"2013-10-17T18:20:20.000-04:00","last_posted_at":"2013-10-17T18:20:21.000-04:00","bumped":true,"bumped_at":"2014-01-14T13:35:37.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":92,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"pekka","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":7}]},{"id":6557,"title":"Middle clicking a link twice does not work as expected","fancy_title":"Middle clicking a link twice does not work as expected","slug":"middle-clicking-a-link-twice-does-not-work-as-expected","posts_count":10,"reply_count":7,"highest_post_number":10,"image_url":null,"created_at":"2013-05-11T13:56:02.000-04:00","last_posted_at":"2014-01-14T13:13:04.000-05:00","bumped":true,"bumped_at":"2014-01-14T13:13:04.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":401,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"neil","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":4780},{"extras":null,"description":"Most Posts","user_id":5053},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":2}]},{"id":11944,"title":"Regression: Cannot sort topic list","fancy_title":"Regression: Cannot sort topic list","slug":"regression-cannot-sort-topic-list","posts_count":5,"reply_count":0,"highest_post_number":5,"image_url":null,"created_at":"2014-01-13T20:14:06.000-05:00","last_posted_at":"2014-01-14T19:31:28.000-05:00","bumped":true,"bumped_at":"2014-01-14T07:31:19.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":true,"views":37,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":1995}]},{"id":10462,"title":"Rebake error when posts contain deleted YouTube video","fancy_title":"Rebake error when posts contain deleted YouTube video","slug":"rebake-error-when-posts-contain-deleted-youtube-video","posts_count":7,"reply_count":1,"highest_post_number":7,"image_url":null,"created_at":"2013-10-19T00:01:21.000-04:00","last_posted_at":"2014-01-14T02:24:19.000-05:00","bumped":true,"bumped_at":"2014-01-14T02:24:12.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":178,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6695},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11932,"title":"Use of blockquote tag causes text outside a paragraph","fancy_title":"Use of blockquote tag causes text outside a paragraph","slug":"use-of-blockquote-tag-causes-text-outside-a-paragraph","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":null,"created_at":"2014-01-13T13:38:15.000-05:00","last_posted_at":"2014-01-13T19:30:37.000-05:00","bumped":true,"bumped_at":"2014-01-14T02:22:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":54,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":10357,"title":"Displaced Wrench Icon Chrome","fancy_title":"Displaced Wrench Icon Chrome","slug":"displaced-wrench-icon-chrome","posts_count":12,"reply_count":4,"highest_post_number":12,"image_url":"/uploads/default/_optimized/9f3/f35/c5379beffe_690x300.jpg","created_at":"2013-10-14T05:48:21.000-04:00","last_posted_at":"2014-01-14T03:21:32.000-05:00","bumped":true,"bumped_at":"2014-01-13T19:03:33.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":206,"like_count":10,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":7073},{"extras":null,"description":"Frequent Poster","user_id":212},{"extras":null,"description":"Frequent Poster","user_id":6118},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":10114,"title":"Invitation expiry workflow is wonky","fancy_title":"Invitation expiry workflow is wonky","slug":"invitation-expiry-workflow-is-wonky","posts_count":14,"reply_count":7,"highest_post_number":14,"image_url":null,"created_at":"2013-09-30T00:59:36.000-04:00","last_posted_at":"2014-01-13T18:51:26.000-05:00","bumped":true,"bumped_at":"2014-01-13T18:51:26.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":176,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":null,"description":"Most Posts","user_id":7076},{"extras":null,"description":"Frequent Poster","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":6330,"title":"Reply not disabled if topic closed while viewing","fancy_title":"Reply not disabled if topic closed while viewing","slug":"reply-not-disabled-if-topic-closed-while-viewing","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon","created_at":"2013-05-02T06:02:06.000-04:00","last_posted_at":"2014-01-13T11:54:22.000-05:00","bumped":true,"bumped_at":"2014-01-13T11:54:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":164,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":4851},{"extras":null,"description":"Most Posts","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":8367,"title":"Very fast scrolling fails to mark all posts read in a thread","fancy_title":"Very fast scrolling fails to mark all posts read in a thread","slug":"very-fast-scrolling-fails-to-mark-all-posts-read-in-a-thread","posts_count":11,"reply_count":7,"highest_post_number":13,"image_url":null,"created_at":"2013-07-14T12:37:02.000-04:00","last_posted_at":"2014-01-13T11:16:56.000-05:00","bumped":true,"bumped_at":"2014-01-13T11:16:33.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":288,"like_count":5,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":4457},{"extras":null,"description":"Most Posts","user_id":6280},{"extras":null,"description":"Frequent Poster","user_id":3681},{"extras":null,"description":"Frequent Poster","user_id":1621},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":8815,"title":"Cache headers confuse proxies","fancy_title":"Cache headers confuse proxies","slug":"cache-headers-confuse-proxies","posts_count":9,"reply_count":3,"highest_post_number":9,"image_url":null,"created_at":"2013-08-02T05:45:26.000-04:00","last_posted_at":"2014-01-13T11:12:09.000-05:00","bumped":true,"bumped_at":"2014-01-13T10:41:44.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":314,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":6266},{"extras":null,"description":"Most Posts","user_id":19},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":4457},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":32}]},{"id":11371,"title":"Search not working for Staff users","fancy_title":"Search not working for Staff users","slug":"search-not-working-for-staff-users","posts_count":15,"reply_count":10,"highest_post_number":15,"image_url":null,"created_at":"2013-12-11T13:22:56.000-05:00","last_posted_at":"2014-01-13T01:41:50.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:41:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":217,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":5335},{"extras":null,"description":"Most Posts","user_id":19},{"extras":null,"description":"Frequent Poster","user_id":6314},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":9908,"title":"Draft bar overrides pagination widget","fancy_title":"Draft bar overrides pagination widget","slug":"draft-bar-overrides-pagination-widget","posts_count":4,"reply_count":0,"highest_post_number":4,"image_url":null,"created_at":"2013-09-19T17:19:52.000-04:00","last_posted_at":"2014-01-13T01:26:01.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:25:12.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":108,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":5351},{"extras":null,"description":"Most Posts","user_id":471},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":6134,"title":"Unread topic is stuck as unread after insertion of staff message","fancy_title":"Unread topic is stuck as unread after insertion of staff message","slug":"unread-topic-is-stuck-as-unread-after-insertion-of-staff-message","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon","created_at":"2013-04-24T13:37:32.000-04:00","last_posted_at":"2014-01-13T01:22:49.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:22:42.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":169,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":3681},{"extras":null,"description":"Most Posts","user_id":5351},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11914,"title":"Google analytics is not registering page views","fancy_title":"Google analytics is not registering page views","slug":"google-analytics-is-not-registering-page-views","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-13T00:32:45.000-05:00","last_posted_at":"2014-01-13T00:32:46.000-05:00","bumped":true,"bumped_at":"2014-01-13T00:32:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":37,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":2,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":1}]}]}}, -"/c/dev/l/latest.json": {"users":[{"id":1,"username":"sam","avatar_template":"/images/avatar.png"},{"id":32,"username":"codinghorror","avatar_template":"/images/avatar.png"},{"id":8021,"username":"Abhishek_Gupta","avatar_template":"/images/avatar.png"},{"id":6695,"username":"illspirit","avatar_template":"/images/avatar.png"},{"id":2,"username":"neil","avatar_template":"/images/avatar.png"},{"id":3124,"username":"sipp11","avatar_template":"/images/avatar.png"},{"id":7513,"username":"digit","avatar_template":"/images/avatar.png"},{"id":19,"username":"eviltrout","avatar_template":"/images/avatar.png"},{"id":3,"username":"supermathie","avatar_template":"/images/avatar.png"},{"id":7073,"username":"5an1ty","avatar_template":"/images/avatar.png"},{"id":4996,"username":"wmertens","avatar_template":"/images/avatar.png"},{"id":6377,"username":"zh99998","avatar_template":"/images/avatar.png"},{"id":1496,"username":"cfstras","avatar_template":"/images/avatar.png"},{"id":7995,"username":"Hunter","avatar_template":"/images/avatar.png"},{"id":6626,"username":"riking","avatar_template":"/images/avatar.png"},{"id":1995,"username":"zogstrip","avatar_template":"/images/avatar.png"},{"id":5048,"username":"SneakySly","avatar_template":"/images/avatar.png"},{"id":7731,"username":"YOU","avatar_template":"/images/avatar.png"},{"id":7985,"username":"onlinedev","avatar_template":"/images/avatar.png"},{"id":3415,"username":"radq","avatar_template":"/images/avatar.png"},{"id":5351,"username":"erlend_sh","avatar_template":"/images/avatar.png"},{"id":471,"username":"BhaelOchon","avatar_template":"/images/avatar.png"},{"id":7,"username":"pekka","avatar_template":"/images/avatar.png"},{"id":4780,"username":"HugoAlmeida","avatar_template":"/images/avatar.png"},{"id":5053,"username":"Blue","avatar_template":"/images/avatar.png"},{"id":212,"username":"alxndr","avatar_template":"/images/avatar.png"},{"id":6118,"username":"lukelarris","avatar_template":"/images/avatar.png"},{"id":7076,"username":"philnelson","avatar_template":"/images/avatar.png"},{"id":4851,"username":"jab","avatar_template":"/images/avatar.png"},{"id":4457,"username":"Lee_Ars","avatar_template":"/images/avatar.png"},{"id":6280,"username":"mx2000","avatar_template":"/images/avatar.png"},{"id":3681,"username":"Ajarn","avatar_template":"/images/avatar.png"},{"id":1621,"username":"bnb","avatar_template":"/images/avatar.png"},{"id":6266,"username":"bragi","avatar_template":"/images/avatar.png"},{"id":5335,"username":"masda70","avatar_template":"/images/avatar.png"},{"id":6314,"username":"rafaelfranca","avatar_template":"/images/avatar.png"}],"topic_list":{"can_create_topic":false,"more_topics_url":"/latest.json?category=2&page=1","draft":null,"draft_key":"new_topic","draft_sequence":null,"topics":[{"id":2,"title":"Category definition for dev","fancy_title":"Category definition for dev","slug":"category-definition-for-dev","posts_count":2,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2013-01-31T23:56:34.000-05:00","last_posted_at":"2013-03-07T22:42:27.000-05:00","bumped":true,"bumped_at":"2013-02-26T18:52:56.000-05:00","unseen":false,"pinned":true,"excerpt":"Development of Discourse.","visible":true,"closed":false,"archived":false,"views":469,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11994,"title":"Cross domain rules, followed?","fancy_title":"Cross domain rules, followed?","slug":"cross-domain-rules-followed","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-16T09:59:15.000-05:00","last_posted_at":"2014-01-16T09:59:15.000-05:00","bumped":true,"bumped_at":"2014-01-16T11:04:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":15,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Abhishek_Gupta","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":8021}]},{"id":11888,"title":"Uncategorized topics not allowed, still seeing tag places","fancy_title":"Uncategorized topics not allowed, still seeing tag places","slug":"uncategorized-topics-not-allowed-still-seeing-tag-places","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2014-01-10T19:23:37.000-05:00","last_posted_at":"2014-01-15T22:41:25.000-05:00","bumped":true,"bumped_at":"2014-01-15T22:41:25.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":50,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"illspirit","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6695},{"extras":null,"description":"Most Posts","user_id":32},{"extras":null,"description":"Frequent Poster","user_id":2}]},{"id":9151,"title":"Apple touch icon doesn't show if there is no sub domain","fancy_title":"Apple touch icon doesn’t show if there is no sub domain","slug":"apple-touch-icon-doesnt-show-if-there-is-no-sub-domain","posts_count":7,"reply_count":4,"highest_post_number":7,"image_url":null,"created_at":"2013-08-16T18:16:53.000-04:00","last_posted_at":"2014-01-15T17:10:18.000-05:00","bumped":true,"bumped_at":"2014-01-15T13:19:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":188,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":3124},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":10911,"title":"/users/activate-account pulling blank logo instead of defaulting to h2","fancy_title":"/users/activate-account pulling blank logo instead of defaulting to h2","slug":"users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2","posts_count":3,"reply_count":1,"highest_post_number":3,"image_url":null,"created_at":"2013-11-12T14:49:04.000-05:00","last_posted_at":"2014-01-15T10:21:37.000-05:00","bumped":true,"bumped_at":"2014-01-15T10:21:37.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":121,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":7513},{"extras":"latest","description":"Most Recent Poster","user_id":19}]},{"id":11937,"title":"Smiley parser is busted","fancy_title":"Smiley parser is busted","slug":"smiley-parser-is-busted","posts_count":4,"reply_count":4,"highest_post_number":7,"image_url":"/plugins/emoji/images/smile.png","created_at":"2014-01-13T15:42:00.000-05:00","last_posted_at":"2014-01-15T05:51:16.000-05:00","bumped":true,"bumped_at":"2014-01-15T05:51:16.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":66,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":3},{"extras":null,"description":"Most Posts","user_id":7073},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":6625,"title":"Error 500 on PUT of site config","fancy_title":"Error 500 on PUT of site config","slug":"error-500-on-put-of-site-config","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2013-05-14T18:13:56.000-04:00","last_posted_at":"2014-01-16T04:55:50.000-05:00","bumped":true,"bumped_at":"2014-01-15T04:43:23.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":132,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":4996},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11225,"title":"Forum acts weirdly after client side updates","fancy_title":"Forum acts weirdly after client side updates","slug":"forum-acts-weirdly-after-client-side-updates","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":null,"created_at":"2013-12-02T18:32:10.000-05:00","last_posted_at":"2014-01-15T04:04:55.000-05:00","bumped":true,"bumped_at":"2014-01-15T02:55:18.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":117,"like_count":7,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11903,"title":"Error after update to 0.9.8.1","fancy_title":"Error after update to 0.9.8.1","slug":"error-after-update-to-0-9-8-1","posts_count":14,"reply_count":6,"highest_post_number":17,"image_url":null,"created_at":"2014-01-12T06:55:45.000-05:00","last_posted_at":"2014-01-15T01:48:58.000-05:00","bumped":true,"bumped_at":"2014-01-15T01:48:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":121,"like_count":6,"has_summary":false,"archetype":"regular","last_poster_username":"zh99998","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":6377},{"extras":null,"description":"Most Posts","user_id":1496},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":19}]},{"id":11969,"title":"Qunit error and possibly related ember.js problem","fancy_title":"Qunit error and possibly related ember.js problem","slug":"qunit-error-and-possibly-related-ember-js-problem","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-14T22:51:32.000-05:00","last_posted_at":"2014-01-14T22:51:32.000-05:00","bumped":false,"bumped_at":"2014-01-14T22:51:32.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":32,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"Hunter","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":7995}]},{"id":11945,"title":"Stuff disappears on the groups page","fancy_title":"Stuff disappears on the groups page","slug":"stuff-disappears-on-the-groups-page","posts_count":7,"reply_count":2,"highest_post_number":7,"image_url":null,"created_at":"2014-01-13T23:03:53.000-05:00","last_posted_at":"2014-01-15T01:26:07.000-05:00","bumped":true,"bumped_at":"2014-01-14T21:09:01.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":54,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":6695},{"extras":null,"description":"Most Posts","user_id":6626},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":1995}]},{"id":11520,"title":"Discourse WordPress Plugin: Emoji's do not properly display","fancy_title":"Discourse WordPress Plugin: Emoji’s do not properly display","slug":"discourse-wordpress-plugin-emojis-do-not-properly-display","posts_count":9,"reply_count":4,"highest_post_number":9,"image_url":"/uploads/default/_optimized/638/4db/eff43a45b8_690x420.png","created_at":"2013-12-19T23:32:03.000-05:00","last_posted_at":"2014-01-15T04:32:19.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:53:34.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":168,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":5048},{"extras":null,"description":"Frequent Poster","user_id":7731},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":11597,"title":"All categories drop down does not close after clicking on first menu \"all categories\"","fancy_title":"All categories drop down does not close after clicking on first menu “all categories”","slug":"all-categories-drop-down-does-not-close-after-clicking-on-first-menu-all-categories","posts_count":5,"reply_count":2,"highest_post_number":5,"image_url":"/uploads/default/2495/f9efe463ae67632d.png","created_at":"2013-12-25T15:09:27.000-05:00","last_posted_at":"2014-01-14T17:46:41.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:46:41.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":73,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"radq","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":7985},{"extras":null,"description":"Most Posts","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":3415}]},{"id":11962,"title":"Editor When Clicking on Wrench Issue","fancy_title":"Editor When Clicking on Wrench Issue","slug":"editor-when-clicking-on-wrench-issue","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/uploads/default/_optimized/ca4/f70/ac7278b8f6_690x176.png","created_at":"2014-01-14T17:23:20.000-05:00","last_posted_at":"2014-01-14T17:24:02.000-05:00","bumped":true,"bumped_at":"2014-01-14T17:24:02.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":30,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":7073},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11831,"title":"Broken links, possibly related to HTTPS","fancy_title":"Broken links, possibly related to HTTPS","slug":"broken-links-possibly-related-to-https","posts_count":17,"reply_count":13,"highest_post_number":18,"image_url":null,"created_at":"2014-01-08T17:40:45.000-05:00","last_posted_at":"2014-01-14T16:03:07.000-05:00","bumped":true,"bumped_at":"2014-01-14T16:03:07.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":102,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":5351},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":471},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":19}]},{"id":11916,"title":"Unable to save user preferences","fancy_title":"Unable to save user preferences","slug":"unable-to-save-user-preferences","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2014-01-13T02:29:26.000-05:00","last_posted_at":"2014-01-14T14:39:32.000-05:00","bumped":true,"bumped_at":"2014-01-14T14:39:29.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":34,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":10425,"title":"Editing category permissions: select value doesn't change","fancy_title":"Editing category permissions: select value doesn’t change","slug":"editing-category-permissions-select-value-doesnt-change","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":"/uploads/meta_discourse/1956/d55fba29dbd7e1fe.png","created_at":"2013-10-17T18:20:20.000-04:00","last_posted_at":"2013-10-17T18:20:21.000-04:00","bumped":true,"bumped_at":"2014-01-14T13:35:37.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":92,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"pekka","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":7}]},{"id":6557,"title":"Middle clicking a link twice does not work as expected","fancy_title":"Middle clicking a link twice does not work as expected","slug":"middle-clicking-a-link-twice-does-not-work-as-expected","posts_count":10,"reply_count":7,"highest_post_number":10,"image_url":null,"created_at":"2013-05-11T13:56:02.000-04:00","last_posted_at":"2014-01-14T13:13:04.000-05:00","bumped":true,"bumped_at":"2014-01-14T13:13:04.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":401,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"neil","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":4780},{"extras":null,"description":"Most Posts","user_id":5053},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":2}]},{"id":11944,"title":"Regression: Cannot sort topic list","fancy_title":"Regression: Cannot sort topic list","slug":"regression-cannot-sort-topic-list","posts_count":5,"reply_count":0,"highest_post_number":5,"image_url":null,"created_at":"2014-01-13T20:14:06.000-05:00","last_posted_at":"2014-01-14T19:31:28.000-05:00","bumped":true,"bumped_at":"2014-01-14T07:31:19.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":true,"views":37,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":1995}]},{"id":10462,"title":"Rebake error when posts contain deleted YouTube video","fancy_title":"Rebake error when posts contain deleted YouTube video","slug":"rebake-error-when-posts-contain-deleted-youtube-video","posts_count":7,"reply_count":1,"highest_post_number":7,"image_url":null,"created_at":"2013-10-19T00:01:21.000-04:00","last_posted_at":"2014-01-14T02:24:19.000-05:00","bumped":true,"bumped_at":"2014-01-14T02:24:12.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":178,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":6695},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11932,"title":"Use of blockquote tag causes text outside a paragraph","fancy_title":"Use of blockquote tag causes text outside a paragraph","slug":"use-of-blockquote-tag-causes-text-outside-a-paragraph","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":null,"created_at":"2014-01-13T13:38:15.000-05:00","last_posted_at":"2014-01-13T19:30:37.000-05:00","bumped":true,"bumped_at":"2014-01-14T02:22:58.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":54,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":6626},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":10357,"title":"Displaced Wrench Icon Chrome","fancy_title":"Displaced Wrench Icon Chrome","slug":"displaced-wrench-icon-chrome","posts_count":12,"reply_count":4,"highest_post_number":12,"image_url":"/uploads/default/_optimized/9f3/f35/c5379beffe_690x300.jpg","created_at":"2013-10-14T05:48:21.000-04:00","last_posted_at":"2014-01-14T03:21:32.000-05:00","bumped":true,"bumped_at":"2014-01-13T19:03:33.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":206,"like_count":10,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":7073},{"extras":null,"description":"Frequent Poster","user_id":212},{"extras":null,"description":"Frequent Poster","user_id":6118},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":"latest","description":"Most Recent Poster, Most Posts","user_id":32}]},{"id":10114,"title":"Invitation expiry workflow is wonky","fancy_title":"Invitation expiry workflow is wonky","slug":"invitation-expiry-workflow-is-wonky","posts_count":14,"reply_count":7,"highest_post_number":14,"image_url":null,"created_at":"2013-09-30T00:59:36.000-04:00","last_posted_at":"2014-01-13T18:51:26.000-05:00","bumped":true,"bumped_at":"2014-01-13T18:51:26.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":176,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":1},{"extras":null,"description":"Most Posts","user_id":7076},{"extras":null,"description":"Frequent Poster","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":6330,"title":"Reply not disabled if topic closed while viewing","fancy_title":"Reply not disabled if topic closed while viewing","slug":"reply-not-disabled-if-topic-closed-while-viewing","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon","created_at":"2013-05-02T06:02:06.000-04:00","last_posted_at":"2014-01-13T11:54:22.000-05:00","bumped":true,"bumped_at":"2014-01-13T11:54:22.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":164,"like_count":1,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":4851},{"extras":null,"description":"Most Posts","user_id":2},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":8367,"title":"Very fast scrolling fails to mark all posts read in a thread","fancy_title":"Very fast scrolling fails to mark all posts read in a thread","slug":"very-fast-scrolling-fails-to-mark-all-posts-read-in-a-thread","posts_count":11,"reply_count":7,"highest_post_number":13,"image_url":null,"created_at":"2013-07-14T12:37:02.000-04:00","last_posted_at":"2014-01-13T11:16:56.000-05:00","bumped":true,"bumped_at":"2014-01-13T11:16:33.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":288,"like_count":5,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":4457},{"extras":null,"description":"Most Posts","user_id":6280},{"extras":null,"description":"Frequent Poster","user_id":3681},{"extras":null,"description":"Frequent Poster","user_id":1621},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":8815,"title":"Cache headers confuse proxies","fancy_title":"Cache headers confuse proxies","slug":"cache-headers-confuse-proxies","posts_count":9,"reply_count":3,"highest_post_number":9,"image_url":null,"created_at":"2013-08-02T05:45:26.000-04:00","last_posted_at":"2014-01-13T11:12:09.000-05:00","bumped":true,"bumped_at":"2014-01-13T10:41:44.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":314,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":6266},{"extras":null,"description":"Most Posts","user_id":19},{"extras":null,"description":"Frequent Poster","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":4457},{"extras":"latest","description":"Most Recent Poster, Frequent Poster","user_id":32}]},{"id":11371,"title":"Search not working for Staff users","fancy_title":"Search not working for Staff users","slug":"search-not-working-for-staff-users","posts_count":15,"reply_count":10,"highest_post_number":15,"image_url":null,"created_at":"2013-12-11T13:22:56.000-05:00","last_posted_at":"2014-01-13T01:41:50.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:41:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":217,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":5335},{"extras":null,"description":"Most Posts","user_id":19},{"extras":null,"description":"Frequent Poster","user_id":6314},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":9908,"title":"Draft bar overrides pagination widget","fancy_title":"Draft bar overrides pagination widget","slug":"draft-bar-overrides-pagination-widget","posts_count":4,"reply_count":0,"highest_post_number":4,"image_url":null,"created_at":"2013-09-19T17:19:52.000-04:00","last_posted_at":"2014-01-13T01:26:01.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:25:12.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":true,"views":108,"like_count":2,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":5351},{"extras":null,"description":"Most Posts","user_id":471},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":6134,"title":"Unread topic is stuck as unread after insertion of staff message","fancy_title":"Unread topic is stuck as unread after insertion of staff message","slug":"unread-topic-is-stuck-as-unread-after-insertion-of-staff-message","posts_count":5,"reply_count":1,"highest_post_number":5,"image_url":"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon","created_at":"2013-04-24T13:37:32.000-04:00","last_posted_at":"2014-01-13T01:22:49.000-05:00","bumped":true,"bumped_at":"2014-01-13T01:22:42.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":true,"archived":false,"views":169,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":3681},{"extras":null,"description":"Most Posts","user_id":5351},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":11914,"title":"Google analytics is not registering page views","fancy_title":"Google analytics is not registering page views","slug":"google-analytics-is-not-registering-page-views","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2014-01-13T00:32:45.000-05:00","last_posted_at":"2014-01-13T00:32:46.000-05:00","bumped":true,"bumped_at":"2014-01-13T00:32:46.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":37,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":1}]}]}}, -"/categories_and_latest.json": {"category_list":{"can_create_category":false,"can_create_topic":false,"draft":null,"draft_key":"new_topic","draft_sequence":null,"categories":[{"id":1,"name":"Uncategorized","color":"AB9364","text_color":"FFFFFF","slug":"uncategorized","topic_count":1,"post_count":0,"position":0,"description":"Topics that don't need a category, or don't fit into any other existing category.","description_text":"","topic_url":null,"logo_url":null,"background_url":null,"read_restricted":false,"permission":null,"notification_level":null,"topic_template":null,"has_children":false,"topics_day":0,"topics_week":0,"topics_month":0,"topics_year":0,"topics_all_time":1,"description_excerpt":"Topics that don't need a category, or don't fit into any other existing category.","is_uncategorized":true},{"id":3,"name":"Site Feedback","color":"808281","text_color":"FFFFFF","slug":"site-feedback","topic_count":0,"post_count":0,"position":1,"description":"Discussion about this site, its organization, how it works, and how we can improve it.","description_text":"Discussion about this site, its organization, how it works, and how we can improve it.","topic_url":"/t/about-the-site-feedback-category/2","logo_url":null,"background_url":null,"read_restricted":false,"permission":null,"notification_level":null,"topic_template":null,"has_children":false,"topics_day":0,"topics_week":0,"topics_month":0,"topics_year":0,"topics_all_time":0,"description_excerpt":"Discussion about this site, its organization, how it works, and how we can improve it."}]},"topic_list":{"can_create_topic":false,"draft":null,"draft_key":"new_topic","draft_sequence":null,"per_page":30,"topics":[{"id":8,"title":"Welcome to Discourse","fancy_title":"Welcome to Discourse","slug":"welcome-to-discourse","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2016-08-29T20:38:19.359Z","last_posted_at":"2016-08-29T20:38:19.402Z","bumped":true,"bumped_at":"2016-08-29T20:38:19.402Z","unseen":false,"pinned":true,"unpinned":null,"excerpt":"The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …","visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"views":0,"like_count":0,"has_summary":false,"archetype":"regular","last_poster_username":"system","category_id":1,"pinned_globally":true,"posters":[{"extras":"latest single","description":"Original Poster, Most Recent Poster","user_id":-1}]}]}} + "/latest.json": { + users: [ + { id: 7204, username: "reyman64", avatar_template: "/images/avatar.png" }, + { id: 1, username: "sam", avatar_template: "/images/avatar.png" }, + { id: 5481, username: "f0rkz", avatar_template: "/images/avatar.png" }, + { id: 6473, username: "jkf", avatar_template: "/images/avatar.png" }, + { + id: 6973, + username: "stellarhopper", + avatar_template: "/images/avatar.png" + }, + { id: 19, username: "eviltrout", avatar_template: "/images/avatar.png" }, + { id: 14, username: "clay", avatar_template: "/images/avatar.png" }, + { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + }, + { id: 1917, username: "sil", avatar_template: "/images/avatar.png" }, + { id: 7197, username: "peeja", avatar_template: "/images/avatar.png" }, + { id: 1995, username: "zogstrip", avatar_template: "/images/avatar.png" }, + { + id: 8021, + username: "Abhishek_Gupta", + avatar_template: "/images/avatar.png" + }, + { id: 2291, username: "PabloC", avatar_template: "/images/avatar.png" }, + { id: 791, username: "srid", avatar_template: "/images/avatar.png" }, + { + id: 1580, + username: "ABillionSuns", + avatar_template: "/images/avatar.png" + }, + { id: 7270, username: "mhurwi", avatar_template: "/images/avatar.png" }, + { + id: 6695, + username: "illspirit", + avatar_template: "/images/avatar.png" + }, + { id: 6929, username: "BCHK", avatar_template: "/images/avatar.png" }, + { id: 4385, username: "jeans", avatar_template: "/images/avatar.png" }, + { id: 7073, username: "5an1ty", avatar_template: "/images/avatar.png" }, + { id: 6626, username: "riking", avatar_template: "/images/avatar.png" }, + { id: 4457, username: "Lee_Ars", avatar_template: "/images/avatar.png" }, + { id: 4263, username: "mcwumbly", avatar_template: "/images/avatar.png" }, + { + id: 8134, + username: "iontishina", + avatar_template: "/images/avatar.png" + }, + { id: 2072, username: "nXqd", avatar_template: "/images/avatar.png" }, + { + id: 4983, + username: "hey_julien", + avatar_template: "/images/avatar.png" + }, + { + id: 3657, + username: "steelmaiden", + avatar_template: "/images/avatar.png" + }, + { id: 2624, username: "BowlingX", avatar_template: "/images/avatar.png" }, + { + id: 8085, + username: "watchmanmonitor", + avatar_template: "/images/avatar.png" + }, + { id: 4612, username: "Iszi", avatar_template: "/images/avatar.png" }, + { + id: 8018, + username: "shivermetimbers", + avatar_template: "/images/avatar.png" + }, + { + id: 6060, + username: "lightyear", + avatar_template: "/images/avatar.png" + }, + { id: 2, username: "neil", avatar_template: "/images/avatar.png" }, + { id: 8037, username: "printec", avatar_template: "/images/avatar.png" }, + { id: 3415, username: "radq", avatar_template: "/images/avatar.png" }, + { + id: 6283, + username: "hrishikesh", + avatar_template: "/images/avatar.png" + }, + { + id: 471, + username: "BhaelOchon", + avatar_template: "/images/avatar.png" + }, + { id: 6548, username: "michaeld", avatar_template: "/images/avatar.png" }, + { + id: 7286, + username: "mrotsnahoj", + avatar_template: "/images/avatar.png" + }, + { id: 3169, username: "dgw", avatar_template: "/images/avatar.png" }, + { + id: 926, + username: "martinnormark", + avatar_template: "/images/avatar.png" + }, + { id: 2003, username: "taylor", avatar_template: "/images/avatar.png" }, + { id: 369, username: "CvX", avatar_template: "/images/avatar.png" }, + { id: 562, username: "nightpool", avatar_template: "/images/avatar.png" }, + { id: 6653, username: "amitfrid", avatar_template: "/images/avatar.png" }, + { + id: 6677, + username: "Tropnevad", + avatar_template: "/images/avatar.png" + }, + { + id: 5048, + username: "SneakySly", + avatar_template: "/images/avatar.png" + }, + { id: 7333, username: "Jong", avatar_template: "/images/avatar.png" }, + { id: 3124, username: "sipp11", avatar_template: "/images/avatar.png" }, + { id: 7604, username: "citkane", avatar_template: "/images/avatar.png" }, + { id: 3929, username: "ScotterC", avatar_template: "/images/avatar.png" }, + { id: 6680, username: "cdman", avatar_template: "/images/avatar.png" }, + { id: 500, username: "aeid", avatar_template: "/images/avatar.png" }, + { id: 8, username: "geek", avatar_template: "/images/avatar.png" }, + { id: 606, username: "Cafeine", avatar_template: "/images/avatar.png" } + ], + topic_list: { + can_create_topic: false, + more_topics_url: "/latest.json?page=1", + draft: null, + draft_key: "new_topic", + draft_sequence: null, + topics: [ + { + id: 11557, + title: "Error after upgrade to 0.9.7.9+", + fancy_title: "Error after upgrade to 0.9.7.9+", + slug: "error-after-upgrade-to-0-9-7-9", + posts_count: 83, + reply_count: 58, + highest_post_number: 85, + image_url: null, + created_at: "2013-12-22T17:12:05.000-05:00", + last_posted_at: "2014-01-16T00:52:30.000-05:00", + bumped: true, + bumped_at: "2014-01-16T00:52:30.000-05:00", + unseen: false, + pinned: true, + excerpt: + "Hi, \n\nI'm using webfaction postgresql specific private instance to run discourse (custom port already configured for discourse 0.9.7.6). \n\nThis is not my first update, but this time i have an error. Impossible to upgrade…", + visible: true, + closed: false, + archived: false, + views: 1230, + like_count: 40, + has_summary: true, + archetype: "regular", + last_poster_username: "stellarhopper", + category_id: 17, + posters: [ + { extras: null, description: "Original Poster", user_id: 7204 }, + { extras: null, description: "Most Posts", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 5481 }, + { extras: null, description: "Frequent Poster", user_id: 6473 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 6973 + } + ] + }, + { + id: 1, + title: "Welcome to meta.discourse.org", + fancy_title: "Welcome to meta.discourse.org", + slug: "welcome-to-meta-discourse-org", + posts_count: 5, + reply_count: 5, + highest_post_number: 23, + image_url: null, + created_at: "2013-01-31T23:52:28.000-05:00", + last_posted_at: "2013-02-07T16:50:41.000-05:00", + bumped: true, + bumped_at: "2013-02-07T11:57:34.000-05:00", + unseen: false, + pinned: true, + excerpt: + "Welcome to meta, the official site for discussing the next-gen open source Discourse forum software. You'll find topics on features, bugs, hosting, development, and general support here. \n\nDiscourse is early beta softwar…", + visible: true, + closed: true, + archived: false, + views: 13792, + like_count: 108, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 17, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { extras: null, description: "Most Posts", user_id: 19 }, + { extras: null, description: "Frequent Poster", user_id: 14 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11997, + title: "Create topic in the future", + fancy_title: "Create topic in the future", + slug: "create-topic-in-the-future", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T12:14:36.000-05:00", + last_posted_at: "2014-01-16T12:14:36.000-05:00", + bumped: false, + bumped_at: "2014-01-16T12:14:36.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 7, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "sil", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 1917 + } + ] + }, + { + id: 11996, + title: + "It's really hard to navigate the Create Topic / Reply pane with the keyboard", + fancy_title: + "It’s really hard to navigate the Create Topic / Reply pane with the keyboard", + slug: + "its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2014-01-16T10:51:36.000-05:00", + last_posted_at: "2014-01-16T11:11:10.000-05:00", + bumped: true, + bumped_at: "2014-01-16T11:11:10.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 12, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 9, + posters: [ + { extras: null, description: "Original Poster", user_id: 7197 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 1995 + } + ] + }, + { + id: 11994, + title: "Cross domain rules, followed?", + fancy_title: "Cross domain rules, followed?", + slug: "cross-domain-rules-followed", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-16T09:59:15.000-05:00", + last_posted_at: "2014-01-16T09:59:15.000-05:00", + bumped: true, + bumped_at: "2014-01-16T11:04:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 15, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Abhishek_Gupta", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 8021 + } + ] + }, + { + id: 11995, + title: "Discourse as a CAS Server", + fancy_title: "Discourse as a CAS Server", + slug: "discourse-as-a-cas-server", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T10:15:30.000-05:00", + last_posted_at: "2014-01-16T10:15:31.000-05:00", + bumped: true, + bumped_at: "2014-01-16T10:15:31.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 12, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "PabloC", + category_id: 6, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 2291 + } + ] + }, + { + id: 11993, + title: "How to check the user level via ajax?", + fancy_title: "How to check the user level via ajax?", + slug: "how-to-check-the-user-level-via-ajax", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T08:13:09.000-05:00", + last_posted_at: "2014-01-16T08:13:09.000-05:00", + bumped: true, + bumped_at: "2014-01-16T09:20:59.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 13, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Abhishek_Gupta", + category_id: 7, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 8021 + } + ] + }, + { + id: 9540, + title: "Docker images for Discourse", + fancy_title: "Docker images for Discourse", + slug: "docker-images-for-discourse", + posts_count: 35, + reply_count: 28, + highest_post_number: 36, + image_url: null, + created_at: "2013-09-02T00:07:02.000-04:00", + last_posted_at: "2014-01-16T07:47:18.000-05:00", + bumped: true, + bumped_at: "2014-01-16T07:47:18.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 1322, + like_count: 23, + has_summary: false, + archetype: "regular", + last_poster_username: "illspirit", + category_id: 8, + posters: [ + { extras: null, description: "Original Poster", user_id: 791 }, + { extras: null, description: "Most Posts", user_id: 1580 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 7270 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 6695 + } + ] + }, + { + id: 11957, + title: "Daily Active Users, Monthly Active Users - Statistics Need", + fancy_title: + "Daily Active Users, Monthly Active Users - Statistics Need", + slug: "daily-active-users-monthly-active-users-statistics-need", + posts_count: 8, + reply_count: 4, + highest_post_number: 8, + image_url: null, + created_at: "2014-01-14T13:40:56.000-05:00", + last_posted_at: "2014-01-16T06:46:05.000-05:00", + bumped: true, + bumped_at: "2014-01-16T06:46:05.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 97, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "jeans", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6929 }, + { extras: null, description: "Most Posts", user_id: 32 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 4385 + } + ] + }, + { + id: 11973, + title: "Pressing Wrench Icon in the Categories section", + fancy_title: "Pressing Wrench Icon in the Categories section", + slug: "pressing-wrench-icon-in-the-categories-section", + posts_count: 6, + reply_count: 3, + highest_post_number: 6, + image_url: "/uploads/default/2907/d8d4e0accd5ee244.png", + created_at: "2014-01-15T05:58:12.000-05:00", + last_posted_at: "2014-01-16T05:15:52.000-05:00", + bumped: true, + bumped_at: "2014-01-16T05:15:52.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 46, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "5an1ty", + category_id: 9, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 7073 + }, + { extras: null, description: "Most Posts", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 6626 } + ] + }, + { + id: 11835, + title: "The Road to Discourse 1.0", + fancy_title: "The Road to Discourse 1.0", + slug: "the-road-to-discourse-1-0", + posts_count: 6, + reply_count: 2, + highest_post_number: 6, + image_url: null, + created_at: "2014-01-08T19:08:44.000-05:00", + last_posted_at: "2014-01-16T04:49:16.000-05:00", + bumped: true, + bumped_at: "2014-01-16T04:49:16.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 421, + like_count: 33, + has_summary: false, + archetype: "regular", + last_poster_username: "iontishina", + category_id: 13, + posters: [ + { extras: null, description: "Original Poster", user_id: 32 }, + { extras: null, description: "Most Posts", user_id: 4457 }, + { extras: null, description: "Frequent Poster", user_id: 4263 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 8134 + } + ] + }, + { + id: 11992, + title: "Specific customization for each category", + fancy_title: "Specific customization for each category", + slug: "specific-customization-for-each-category", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T04:04:58.000-05:00", + last_posted_at: "2014-01-16T04:04:58.000-05:00", + bumped: false, + bumped_at: "2014-01-16T04:04:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 18, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "nXqd", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 2072 + } + ] + }, + { + id: 9214, + title: "Please make category url shorter", + fancy_title: "Please make category url shorter", + slug: "please-make-category-url-shorter", + posts_count: 9, + reply_count: 3, + highest_post_number: 9, + image_url: null, + created_at: "2013-08-20T05:28:17.000-04:00", + last_posted_at: "2014-01-16T04:02:46.000-05:00", + bumped: true, + bumped_at: "2014-01-16T04:02:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 319, + like_count: 13, + has_summary: false, + archetype: "regular", + last_poster_username: "nXqd", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 4983 }, + { extras: null, description: "Most Posts", user_id: 3657 }, + { extras: null, description: "Frequent Poster", user_id: 2624 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 2072 + } + ] + }, + { + id: 11989, + title: "Where to change the email subject prefix", + fancy_title: "Where to change the email subject prefix", + slug: "where-to-change-the-email-subject-prefix", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: "/uploads/default/2919/adbfe0ff90353440.png", + created_at: "2014-01-16T01:03:48.000-05:00", + last_posted_at: "2014-01-16T03:20:09.000-05:00", + bumped: true, + bumped_at: "2014-01-16T03:20:09.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 19, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 6, + posters: [ + { extras: null, description: "Original Poster", user_id: 8085 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 10866, + title: "Header logo overflows the top header area", + fancy_title: "Header logo overflows the top header area", + slug: "header-logo-overflows-the-top-header-area", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2013-11-09T03:40:04.000-05:00", + last_posted_at: "2014-01-16T02:27:52.000-05:00", + bumped: true, + bumped_at: "2014-01-16T02:40:47.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 157, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "stellarhopper", + category_id: 6, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6973 + }, + { extras: null, description: "Most Posts", user_id: 32 } + ] + }, + { + id: 11988, + title: "Could not locate Gemfile error", + fancy_title: "Could not locate Gemfile error", + slug: "could-not-locate-gemfile-error", + posts_count: 7, + reply_count: 3, + highest_post_number: 7, + image_url: null, + created_at: "2014-01-16T00:41:57.000-05:00", + last_posted_at: "2014-01-16T01:20:46.000-05:00", + bumped: true, + bumped_at: "2014-01-16T01:20:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 18, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 6, + posters: [ + { extras: null, description: "Original Poster", user_id: 6973 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 6266, + title: "What sort of replies trigger a notice?", + fancy_title: "What sort of replies trigger a notice?", + slug: "what-sort-of-replies-trigger-a-notice", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2013-04-30T17:46:39.000-04:00", + last_posted_at: "2014-01-16T00:52:21.000-05:00", + bumped: true, + bumped_at: "2014-01-16T00:57:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 115, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 17, + posters: [ + { extras: null, description: "Original Poster", user_id: 4612 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11610, + title: "Private replies that only admins can see", + fancy_title: "Private replies that only admins can see", + slug: "private-replies-that-only-admins-can-see", + posts_count: 21, + reply_count: 20, + highest_post_number: 23, + image_url: null, + created_at: "2013-12-26T20:31:10.000-05:00", + last_posted_at: "2014-01-16T00:18:19.000-05:00", + bumped: true, + bumped_at: "2014-01-16T00:18:19.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 206, + like_count: 9, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 8018 }, + { extras: null, description: "Most Posts", user_id: 4263 }, + { extras: null, description: "Frequent Poster", user_id: 6060 }, + { extras: null, description: "Frequent Poster", user_id: 6626 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11888, + title: "Uncategorized topics not allowed, still seeing tag places", + fancy_title: + "Uncategorized topics not allowed, still seeing tag places", + slug: "uncategorized-topics-not-allowed-still-seeing-tag-places", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-10T19:23:37.000-05:00", + last_posted_at: "2014-01-15T22:41:25.000-05:00", + bumped: true, + bumped_at: "2014-01-15T22:41:25.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 50, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "illspirit", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6695 + }, + { extras: null, description: "Most Posts", user_id: 32 }, + { extras: null, description: "Frequent Poster", user_id: 2 } + ] + }, + { + id: 11985, + title: + "Installation nearly installs on Centos 6.5 with Apache/Phusion", + fancy_title: + "Installation nearly installs on Centos 6.5 with Apache/Phusion", + slug: + "installation-nearly-installs-on-centos-6-5-with-apache-phusion", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-15T19:48:30.000-05:00", + last_posted_at: "2014-01-15T19:48:30.000-05:00", + bumped: false, + bumped_at: "2014-01-15T19:48:30.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 26, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "printec", + category_id: 6, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 8037 + } + ] + }, + { + id: 11981, + title: "Excluding categories from the top view?", + fancy_title: "Excluding categories from the top view?", + slug: "excluding-categories-from-the-top-view", + posts_count: 6, + reply_count: 1, + highest_post_number: 6, + image_url: + "/uploads/default/_optimized/f01/22f/7ea01f77b9_690x355.png", + created_at: "2014-01-15T15:01:37.000-05:00", + last_posted_at: "2014-01-15T18:57:52.000-05:00", + bumped: true, + bumped_at: "2014-01-15T18:57:47.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 43, + like_count: 6, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 3415 }, + { extras: null, description: "Most Posts", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 1995 + } + ] + }, + { + id: 9408, + title: "Different home page for regular vs. new user", + fancy_title: "Different home page for regular vs. new user", + slug: "different-home-page-for-regular-vs-new-user", + posts_count: 25, + reply_count: 17, + highest_post_number: 25, + image_url: null, + created_at: "2013-08-28T09:54:41.000-04:00", + last_posted_at: "2014-01-15T18:33:16.000-05:00", + bumped: true, + bumped_at: "2014-01-15T18:33:16.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 334, + like_count: 21, + has_summary: false, + archetype: "regular", + last_poster_username: "mcwumbly", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6283 }, + { extras: null, description: "Most Posts", user_id: 32 }, + { extras: null, description: "Frequent Poster", user_id: 1995 }, + { extras: null, description: "Frequent Poster", user_id: 471 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 4263 + } + ] + }, + { + id: 11896, + title: "Problem creating new account", + fancy_title: "Problem creating new account", + slug: "problem-creating-new-account", + posts_count: 11, + reply_count: 2, + highest_post_number: 11, + image_url: null, + created_at: "2014-01-11T09:07:20.000-05:00", + last_posted_at: "2014-01-15T20:50:05.000-05:00", + bumped: true, + bumped_at: "2014-01-15T15:23:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 87, + like_count: 6, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 6, + posters: [ + { extras: null, description: "Original Poster", user_id: 6548 }, + { extras: null, description: "Most Posts", user_id: 32 }, + { extras: null, description: "Frequent Poster", user_id: 2 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 10511, + title: "External urls should open in new tab", + fancy_title: "External urls should open in new tab", + slug: "external-urls-should-open-in-new-tab", + posts_count: 7, + reply_count: 3, + highest_post_number: 7, + image_url: null, + created_at: "2013-10-20T14:54:27.000-04:00", + last_posted_at: "2014-01-15T14:02:11.000-05:00", + bumped: true, + bumped_at: "2014-01-15T14:01:55.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 242, + like_count: 10, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 7286 }, + { extras: null, description: "Most Posts", user_id: 3169 }, + { extras: null, description: "Frequent Poster", user_id: 4263 }, + { extras: null, description: "Frequent Poster", user_id: 6626 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 1589, + title: "Keyboard shortcuts?", + fancy_title: "Keyboard shortcuts?", + slug: "keyboard-shortcuts", + posts_count: 19, + reply_count: 10, + highest_post_number: 20, + image_url: null, + created_at: "2013-02-06T14:05:01.000-05:00", + last_posted_at: "2014-01-15T13:52:45.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:52:45.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 754, + like_count: 31, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 926 }, + { extras: null, description: "Most Posts", user_id: 2003 }, + { extras: null, description: "Frequent Poster", user_id: 369 }, + { extras: null, description: "Frequent Poster", user_id: 562 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11763, + title: "Google AdSense plugin is now available", + fancy_title: "Google AdSense plugin is now available", + slug: "google-adsense-plugin-is-now-available", + posts_count: 7, + reply_count: 2, + highest_post_number: 7, + image_url: + "/uploads/default/_optimized/66d/cf0/d69e6709fe_496x500.PNG", + created_at: "2014-01-05T14:28:58.000-05:00", + last_posted_at: "2014-01-15T13:32:35.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:32:35.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 213, + like_count: 14, + has_summary: false, + archetype: "regular", + last_poster_username: "michaeld", + category_id: 5, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6548 + }, + { extras: null, description: "Most Posts", user_id: 6653 }, + { extras: null, description: "Frequent Poster", user_id: 6677 }, + { extras: null, description: "Frequent Poster", user_id: 5048 }, + { extras: null, description: "Frequent Poster", user_id: 7333 } + ] + }, + { + id: 9151, + title: "Apple touch icon doesn't show if there is no sub domain", + fancy_title: + "Apple touch icon doesn’t show if there is no sub domain", + slug: "apple-touch-icon-doesnt-show-if-there-is-no-sub-domain", + posts_count: 7, + reply_count: 4, + highest_post_number: 7, + image_url: null, + created_at: "2013-08-16T18:16:53.000-04:00", + last_posted_at: "2014-01-15T17:10:18.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:19:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 188, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 3124 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11977, + title: "Show subcategory topics in categories list summary", + fancy_title: "Show subcategory topics in categories list summary", + slug: "show-subcategory-topics-in-categories-list-summary", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: + "/uploads/default/_optimized/084/4e4/8af88c0839_571x500.png", + created_at: "2014-01-15T12:09:49.000-05:00", + last_posted_at: "2014-01-15T12:50:04.000-05:00", + bumped: true, + bumped_at: "2014-01-15T12:50:04.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 32, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 7604 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 10201, + title: "How To override an existing handlebars template from plugin", + fancy_title: + "How To override an existing handlebars template from plugin", + slug: "how-to-override-an-existing-handlebars-template-from-plugin", + posts_count: 6, + reply_count: 1, + highest_post_number: 6, + image_url: null, + created_at: "2013-10-04T10:44:33.000-04:00", + last_posted_at: "2014-01-15T12:35:01.000-05:00", + bumped: true, + bumped_at: "2014-01-15T12:34:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 325, + like_count: 6, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 3929 }, + { extras: null, description: "Most Posts", user_id: 3415 }, + { extras: null, description: "Frequent Poster", user_id: 6680 }, + { extras: null, description: "Frequent Poster", user_id: 2 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 1995 + } + ] + }, + { + id: 531, + title: "Discourse and Wordpress Integration", + fancy_title: "Discourse and Wordpress Integration", + slug: "discourse-and-wordpress-integration", + posts_count: 76, + reply_count: 64, + highest_post_number: 78, + image_url: null, + created_at: "2013-02-05T18:56:37.000-05:00", + last_posted_at: "2014-01-15T11:56:54.000-05:00", + bumped: true, + bumped_at: "2014-01-15T11:56:54.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 3809, + like_count: 84, + has_summary: true, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 5, + posters: [ + { extras: null, description: "Original Poster", user_id: 500 }, + { extras: null, description: "Most Posts", user_id: 8 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 606 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + } + ] + } + }, + "/categories.json": { + category_list: { + can_create_category: false, + can_create_topic: false, + draft: null, + draft_key: "new_topic", + draft_sequence: null, + categories: [ + { + id: 1, + name: "bug", + color: "e9dd00", + text_color: "000000", + slug: "bug", + topic_count: 660, + description: + "Bug reports on Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", + topic_url: "/t/category-definition-for-bug/2", + read_restricted: false, + permission: null, + post_count: 4318, + topics_day: 0, + topics_week: 18, + topics_month: 54, + topics_year: 658, + posts_day: 0, + posts_week: 330, + posts_month: 574, + posts_year: 4319, + description_excerpt: + "Bug reports on Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", + featured_user_ids: [8021, 32, 6695, 2, 1995], + topics: [ + { + id: 11994, + title: "Cross domain rules, followed?", + fancy_title: "Cross domain rules, followed?", + slug: "cross-domain-rules-followed", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-16T09:59:15.000-05:00", + last_posted_at: "2014-01-16T09:59:15.000-05:00", + bumped: true, + bumped_at: "2014-01-16T11:04:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 8021, + username: "Abhishek_Gupta", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11888, + title: + "Uncategorized topics not allowed, still seeing tag places", + fancy_title: + "Uncategorized topics not allowed, still seeing tag places", + slug: "uncategorized-topics-not-allowed-still-seeing-tag-places", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-10T19:23:37.000-05:00", + last_posted_at: "2014-01-15T22:41:25.000-05:00", + bumped: true, + bumped_at: "2014-01-15T22:41:25.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6695, + username: "illspirit", + avatar_template: "/images/avatar.png" + } + }, + { + id: 9151, + title: "Apple touch icon doesn't show if there is no sub domain", + fancy_title: + "Apple touch icon doesn’t show if there is no sub domain", + slug: "apple-touch-icon-doesnt-show-if-there-is-no-sub-domain", + posts_count: 7, + reply_count: 4, + highest_post_number: 7, + image_url: null, + created_at: "2013-08-16T18:16:53.000-04:00", + last_posted_at: "2014-01-15T17:10:18.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:19:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 2, + name: "feature", + color: "0E76BD", + text_color: "FFFFFF", + slug: "feature", + topic_count: 727, + description: + "Discussion about features or potential features of Discourse: how they work, why they work, etc.", + topic_url: "/t/category-definition-for-feature/11", + read_restricted: false, + permission: null, + post_count: 6186, + topics_day: 0, + topics_week: 17, + topics_month: 46, + topics_year: 725, + posts_day: 0, + posts_week: 180, + posts_month: 468, + posts_year: 6187, + description_excerpt: + "Discussion about features or potential features of Discourse: how they work, why they work, etc.", + featured_user_ids: [1917, 4385, 2072, 32, 4263], + topics: [ + { + id: 11997, + title: "Create topic in the future", + fancy_title: "Create topic in the future", + slug: "create-topic-in-the-future", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T12:14:36.000-05:00", + last_posted_at: "2014-01-16T12:14:36.000-05:00", + bumped: false, + bumped_at: "2014-01-16T12:14:36.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 1917, + username: "sil", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11957, + title: + "Daily Active Users, Monthly Active Users - Statistics Need", + fancy_title: + "Daily Active Users, Monthly Active Users - Statistics Need", + slug: "daily-active-users-monthly-active-users-statistics-need", + posts_count: 8, + reply_count: 4, + highest_post_number: 8, + image_url: null, + created_at: "2014-01-14T13:40:56.000-05:00", + last_posted_at: "2014-01-16T06:46:05.000-05:00", + bumped: true, + bumped_at: "2014-01-16T06:46:05.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 4385, + username: "jeans", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11992, + title: "Specific customization for each category", + fancy_title: "Specific customization for each category", + slug: "specific-customization-for-each-category", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T04:04:58.000-05:00", + last_posted_at: "2014-01-16T04:04:58.000-05:00", + bumped: false, + bumped_at: "2014-01-16T04:04:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 2072, + username: "nXqd", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 6, + name: "support", + color: "b99", + text_color: "FFFFFF", + slug: "support", + topic_count: 782, + description: + "Support on configuring, using, and installing Discourse. Not for software development related topics, but for admins and end users configuring and using Discourse.", + topic_url: "/t/category-definition-for-support/389", + read_restricted: false, + permission: null, + post_count: 5396, + topics_day: 0, + topics_week: 16, + topics_month: 67, + topics_year: 779, + posts_day: 0, + posts_week: 122, + posts_month: 481, + posts_year: 5400, + description_excerpt: + "Support on configuring, using, and installing Discourse. Not for software development related topics, but for admins and end users configuring and using Discourse.", + featured_user_ids: [2291, 32, 6973, 1, 8085], + topics: [ + { + id: 11995, + title: "Discourse as a CAS Server", + fancy_title: "Discourse as a CAS Server", + slug: "discourse-as-a-cas-server", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T10:15:30.000-05:00", + last_posted_at: "2014-01-16T10:15:31.000-05:00", + bumped: true, + bumped_at: "2014-01-16T10:15:31.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 2291, + username: "PabloC", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11989, + title: "Where to change the email subject prefix", + fancy_title: "Where to change the email subject prefix", + slug: "where-to-change-the-email-subject-prefix", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: "/uploads/default/2919/adbfe0ff90353440.png", + created_at: "2014-01-16T01:03:48.000-05:00", + last_posted_at: "2014-01-16T03:20:09.000-05:00", + bumped: true, + bumped_at: "2014-01-16T03:20:09.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + }, + { + id: 10866, + title: "Header logo overflows the top header area", + fancy_title: "Header logo overflows the top header area", + slug: "header-logo-overflows-the-top-header-area", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2013-11-09T03:40:04.000-05:00", + last_posted_at: "2014-01-16T02:27:52.000-05:00", + bumped: true, + bumped_at: "2014-01-16T02:40:47.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6973, + username: "stellarhopper", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 7, + name: "dev", + color: "000", + text_color: "FFFFFF", + slug: "dev", + topic_count: 284, + description: + "This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.", + topic_url: "/t/category-definition-for-dev/1026", + read_restricted: false, + permission: null, + post_count: 2352, + topics_day: 0, + topics_week: 3, + topics_month: 19, + topics_year: 284, + posts_day: 0, + posts_week: 37, + posts_month: 150, + posts_year: 2353, + description_excerpt: + "This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.", + featured_user_ids: [8021, 1995, 5428, 8208, 7995], + topics: [ + { + id: 3823, + title: "So, you want to help out with Discourse", + fancy_title: "So, you want to help out with Discourse", + slug: "so-you-want-to-help-out-with-discourse", + posts_count: 22, + reply_count: 28, + highest_post_number: 56, + image_url: null, + created_at: "2013-02-23T00:46:11.000-05:00", + last_posted_at: "2014-01-12T21:33:12.000-05:00", + bumped: true, + bumped_at: "2014-01-12T21:33:12.000-05:00", + unseen: false, + pinned: true, + excerpt: + "People are wondering, how it is they can help out with Discourse. \n\nWe have seen some chattering both here and on Github. \n\nI wanted to create a topic @eviltrout , @codinghorror and myself can keep up to date with clear…", + visible: true, + closed: false, + archived: false, + last_poster: { + id: 7995, + username: "Hunter", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11993, + title: "How to check the user level via ajax?", + fancy_title: "How to check the user level via ajax?", + slug: "how-to-check-the-user-level-via-ajax", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-16T08:13:09.000-05:00", + last_posted_at: "2014-01-16T08:13:09.000-05:00", + bumped: true, + bumped_at: "2014-01-16T09:20:59.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 8021, + username: "Abhishek_Gupta", + avatar_template: "/images/avatar.png" + } + }, + { + id: 10201, + title: + "How To override an existing handlebars template from plugin", + fancy_title: + "How To override an existing handlebars template from plugin", + slug: + "how-to-override-an-existing-handlebars-template-from-plugin", + posts_count: 6, + reply_count: 1, + highest_post_number: 6, + image_url: null, + created_at: "2013-10-04T10:44:33.000-04:00", + last_posted_at: "2014-01-15T12:35:01.000-05:00", + bumped: true, + bumped_at: "2014-01-15T12:34:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + last_poster: { + id: 1995, + username: "zogstrip", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 9, + name: "ux", + color: "5F497A", + text_color: "FFFFFF", + slug: "ux", + topic_count: 184, + description: + "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", + topic_url: "/t/category-definition-for-ux/2628", + read_restricted: false, + permission: null, + post_count: 1511, + topics_day: 0, + topics_week: 3, + topics_month: 10, + topics_year: 183, + posts_day: 0, + posts_week: 34, + posts_month: 117, + posts_year: 1511, + description_excerpt: + "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", + featured_user_ids: [1995, 7197, 7073, 1, 6626], + topics: [ + { + id: 11996, + title: + "It's really hard to navigate the Create Topic / Reply pane with the keyboard", + fancy_title: + "It’s really hard to navigate the Create Topic / Reply pane with the keyboard", + slug: + "its-really-hard-to-navigate-the-create-topic-reply-pane-with-the-keyboard", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2014-01-16T10:51:36.000-05:00", + last_posted_at: "2014-01-16T11:11:10.000-05:00", + bumped: true, + bumped_at: "2014-01-16T11:11:10.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 1995, + username: "zogstrip", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11973, + title: "Pressing Wrench Icon in the Categories section", + fancy_title: "Pressing Wrench Icon in the Categories section", + slug: "pressing-wrench-icon-in-the-categories-section", + posts_count: 6, + reply_count: 3, + highest_post_number: 6, + image_url: "/uploads/default/2907/d8d4e0accd5ee244.png", + created_at: "2014-01-15T05:58:12.000-05:00", + last_posted_at: "2014-01-16T05:15:52.000-05:00", + bumped: true, + bumped_at: "2014-01-16T05:15:52.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 7073, + username: "5an1ty", + avatar_template: "/images/avatar.png" + } + }, + { + id: 5542, + title: "Title character requirements not very visible", + fancy_title: "Title character requirements not very visible", + slug: "title-character-requirements-not-very-visible", + posts_count: 24, + reply_count: 11, + highest_post_number: 24, + image_url: null, + created_at: "2013-04-02T20:09:59.000-04:00", + last_posted_at: "2014-01-15T05:26:07.000-05:00", + bumped: true, + bumped_at: "2014-01-15T05:26:04.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + last_poster: { + id: 1995, + username: "zogstrip", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 5, + name: "extensibility", + color: "FE8432", + text_color: "FFFFFF", + slug: "extensibility", + topic_count: 102, + description: + "Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ", + topic_url: "/t/category-definition-for-extensibility/28", + read_restricted: false, + permission: null, + post_count: 964, + topics_day: 0, + topics_week: 2, + topics_month: 18, + topics_year: 102, + posts_day: 0, + posts_week: 17, + posts_month: 76, + posts_year: 964, + description_excerpt: + "Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility.", + featured_user_ids: [6548, 32, 8202, 6677, 7333], + topics: [ + { + id: 11763, + title: "Google AdSense plugin is now available", + fancy_title: "Google AdSense plugin is now available", + slug: "google-adsense-plugin-is-now-available", + posts_count: 7, + reply_count: 2, + highest_post_number: 7, + image_url: + "/uploads/default/_optimized/66d/cf0/d69e6709fe_496x500.PNG", + created_at: "2014-01-05T14:28:58.000-05:00", + last_posted_at: "2014-01-15T13:32:35.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:32:35.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6548, + username: "michaeld", + avatar_template: "/images/avatar.png" + } + }, + { + id: 531, + title: "Discourse and Wordpress Integration", + fancy_title: "Discourse and Wordpress Integration", + slug: "discourse-and-wordpress-integration", + posts_count: 76, + reply_count: 64, + highest_post_number: 78, + image_url: null, + created_at: "2013-02-05T18:56:37.000-05:00", + last_posted_at: "2014-01-15T11:56:54.000-05:00", + bumped: true, + bumped_at: "2014-01-15T11:56:54.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11965, + title: + "In your opinion, what is the best wiki engine to be associated with discourse?", + fancy_title: + "In your opinion, what is the best wiki engine to be associated with discourse?", + slug: + "in-your-opinion-what-is-the-best-wiki-engine-to-be-associated-with-discourse", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-14T19:27:06.000-05:00", + last_posted_at: "2014-01-14T19:27:06.000-05:00", + bumped: false, + bumped_at: "2014-01-14T19:27:06.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 8202, + username: "Matthieu", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 8, + name: "hosting", + color: "74CCED", + text_color: "FFFFFF", + slug: "hosting", + topic_count: 69, + description: + "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services.", + topic_url: "/t/category-definition-for-hosting/2626", + read_restricted: false, + permission: null, + post_count: 664, + topics_day: 0, + topics_week: 2, + topics_month: 2, + topics_year: 69, + posts_day: 0, + posts_week: 15, + posts_month: 35, + posts_year: 664, + description_excerpt: + "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services.", + featured_user_ids: [6695, 1, 6018, 1580, 7030], + topics: [ + { + id: 9540, + title: "Docker images for Discourse", + fancy_title: "Docker images for Discourse", + slug: "docker-images-for-discourse", + posts_count: 35, + reply_count: 28, + highest_post_number: 36, + image_url: null, + created_at: "2013-09-02T00:07:02.000-04:00", + last_posted_at: "2014-01-16T07:47:18.000-05:00", + bumped: true, + bumped_at: "2014-01-16T07:47:18.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6695, + username: "illspirit", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11971, + title: + "Installing Discourse on Ubuntu 12.04 with Parallels Plesk and Apache", + fancy_title: + "Installing Discourse on Ubuntu 12.04 with Parallels Plesk and Apache", + slug: + "installing-discourse-on-ubuntu-12-04-with-parallels-plesk-and-apache", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: null, + created_at: "2014-01-15T04:23:38.000-05:00", + last_posted_at: "2014-01-15T04:47:20.000-05:00", + bumped: true, + bumped_at: "2014-01-15T04:47:20.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 7030, + username: "naabster", + avatar_template: "/images/avatar.png" + } + }, + { + id: 10844, + title: "Discourse in a Docker container", + fancy_title: "Discourse in a Docker container", + slug: "discourse-in-a-docker-container", + posts_count: 12, + reply_count: 8, + highest_post_number: 12, + image_url: null, + created_at: "2013-11-07T19:12:22.000-05:00", + last_posted_at: "2014-01-11T14:43:53.000-05:00", + bumped: true, + bumped_at: "2014-01-11T14:43:53.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 1, + username: "sam", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 17, + name: "uncategorized", + color: "AB9364", + text_color: "FFFFFF", + slug: "uncategorized", + topic_count: 229, + description: "", + topic_url: null, + read_restricted: false, + permission: null, + post_count: 2138, + topics_day: 0, + topics_week: 0, + topics_month: 9, + topics_year: 229, + posts_day: 1, + posts_week: 11, + posts_month: 183, + posts_year: 2138, + description_excerpt: "", + is_uncategorized: true, + featured_user_ids: [6973, 32, 1, 1995, 7073], + topics: [ + { + id: 1, + title: "Welcome to meta.discourse.org", + fancy_title: "Welcome to meta.discourse.org", + slug: "welcome-to-meta-discourse-org", + posts_count: 5, + reply_count: 5, + highest_post_number: 23, + image_url: null, + created_at: "2013-01-31T23:52:28.000-05:00", + last_posted_at: "2013-02-07T16:50:41.000-05:00", + bumped: true, + bumped_at: "2013-02-07T11:57:34.000-05:00", + unseen: false, + pinned: true, + excerpt: + "Welcome to meta, the official site for discussing the next-gen open source Discourse forum software. You'll find topics on features, bugs, hosting, development, and general support here. \n\nDiscourse is early beta softwar…", + visible: true, + closed: true, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11557, + title: "Error after upgrade to 0.9.7.9+", + fancy_title: "Error after upgrade to 0.9.7.9+", + slug: "error-after-upgrade-to-0-9-7-9", + posts_count: 83, + reply_count: 58, + highest_post_number: 85, + image_url: null, + created_at: "2013-12-22T17:12:05.000-05:00", + last_posted_at: "2014-01-16T00:52:30.000-05:00", + bumped: true, + bumped_at: "2014-01-16T00:52:30.000-05:00", + unseen: false, + pinned: true, + excerpt: + "Hi, \n\nI'm using webfaction postgresql specific private instance to run discourse (custom port already configured for discourse 0.9.7.6). \n\nThis is not my first update, but this time i have an error. Impossible to upgrade…", + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6973, + username: "stellarhopper", + avatar_template: "/images/avatar.png" + } + }, + { + id: 6266, + title: "What sort of replies trigger a notice?", + fancy_title: "What sort of replies trigger a notice?", + slug: "what-sort-of-replies-trigger-a-notice", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2013-04-30T17:46:39.000-04:00", + last_posted_at: "2014-01-16T00:52:21.000-05:00", + bumped: true, + bumped_at: "2014-01-16T00:57:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 11, + name: "login", + color: "edb400", + text_color: "FFFFFF", + slug: "login", + topic_count: 27, + description: + "Topics about logging in to Discourse, using any standard third party provider (Twitter, Facebook, Google), traditional username and password, or with a custom plugin.", + topic_url: "/t/category-definition-for-login/2828", + read_restricted: false, + permission: null, + post_count: 200, + topics_day: 0, + topics_week: 1, + topics_month: 1, + topics_year: 27, + posts_day: 0, + posts_week: 10, + posts_month: 27, + posts_year: 200, + description_excerpt: + "Topics about logging in to Discourse, using any standard third party provider (Twitter, Facebook, Google), traditional username and password, or with a custom plugin.", + featured_user_ids: [8163, 19, 7796, 32, 8024], + topics: [ + { + id: 11959, + title: "Get current user information via JSON", + fancy_title: "Get current user information via JSON", + slug: "get-current-user-information-via-json", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: null, + created_at: "2014-01-14T15:05:34.000-05:00", + last_posted_at: "2014-01-14T16:43:28.000-05:00", + bumped: true, + bumped_at: "2014-01-14T16:43:28.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 8163, + username: "znation", + avatar_template: "/images/avatar.png" + } + }, + { + id: 6242, + title: + "Allow authentication via multiple services on one account", + fancy_title: + "Allow authentication via multiple services on one account", + slug: "allow-authentication-via-multiple-services-on-one-account", + posts_count: 34, + reply_count: 27, + highest_post_number: 34, + image_url: null, + created_at: "2013-04-29T18:51:52.000-04:00", + last_posted_at: "2014-01-14T00:25:42.000-05:00", + bumped: true, + bumped_at: "2014-01-14T00:25:42.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 7796, + username: "almereyda", + avatar_template: "/images/avatar.png" + } + }, + { + id: 4738, + title: "Login support for browser password managers", + fancy_title: "Login support for browser password managers", + slug: "login-support-for-browser-password-managers", + posts_count: 6, + reply_count: 2, + highest_post_number: 6, + image_url: null, + created_at: "2013-03-13T17:55:29.000-04:00", + last_posted_at: "2014-01-13T14:21:34.000-05:00", + bumped: true, + bumped_at: "2014-01-13T14:21:34.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 3, + name: "meta", + color: "aaa", + text_color: "FFFFFF", + slug: "meta", + topic_count: 79, + description: + "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.", + topic_url: "/t/category-definition-for-meta/24", + read_restricted: false, + permission: null, + post_count: 695, + topics_day: 0, + topics_week: 1, + topics_month: 3, + topics_year: 79, + posts_day: 0, + posts_week: 4, + posts_month: 18, + posts_year: 696, + description_excerpt: + "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.", + featured_user_ids: [19, 8085, 32, 5174, 4534], + topics: [ + { + id: 5249, + title: 'What is "Meta"?', + fancy_title: "What is “Meta”?", + slug: "what-is-meta", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2013-03-25T18:00:52.000-04:00", + last_posted_at: "2013-03-25T18:00:56.000-04:00", + bumped: false, + bumped_at: "2013-03-25T18:00:52.000-04:00", + unseen: false, + pinned: true, + excerpt: + "Meta means discussion of the discussion itself instead of the actual topic of the discussion. \n\nWhy do we need a meta category?\n\nMeta is where communities come together to decide who they are and what they are about. \n…", + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11943, + title: "How far to take user documentation?", + fancy_title: "How far to take user documentation?", + slug: "how-far-to-take-user-documentation", + posts_count: 4, + reply_count: 2, + highest_post_number: 4, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-13T19:21:26.000-05:00", + last_posted_at: "2014-01-14T14:19:46.000-05:00", + bumped: true, + bumped_at: "2014-01-14T14:19:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 19, + username: "eviltrout", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11822, + title: "Search engine traffic share and level to Discourse", + fancy_title: "Search engine traffic share and level to Discourse", + slug: "search-engine-traffic-share-and-level-to-discourse", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2014-01-08T01:54:56.000-05:00", + last_posted_at: "2014-01-08T02:21:25.000-05:00", + bumped: true, + bumped_at: "2014-01-08T02:21:25.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 12, + name: "discourse hub", + color: "b2c79f", + text_color: "FFFFFF", + slug: "discourse-hub", + topic_count: 4, + description: + "Topics about current or future Discourse Hub functionality at discourse.org including nickname registration, global user pages, and the site directory.", + topic_url: "/t/category-definition-for-discourse-hub/3038", + read_restricted: false, + permission: null, + post_count: 121, + topics_day: 0, + topics_week: 0, + topics_month: 0, + topics_year: 4, + posts_day: 0, + posts_week: 3, + posts_month: 3, + posts_year: 121, + description_excerpt: + "Topics about current or future Discourse Hub functionality at discourse.org including nickname registration, global user pages, and the site directory.", + featured_user_ids: [2, 32, 2316, 6695, 4457], + topics: [ + { + id: 6547, + title: "Where to get discourse_org_access_key?", + fancy_title: "Where to get discourse_org_access_key?", + slug: "where-to-get-discourse-org-access-key", + posts_count: 13, + reply_count: 4, + highest_post_number: 13, + image_url: null, + created_at: "2013-05-10T22:06:08.000-04:00", + last_posted_at: "2014-01-13T11:38:15.000-05:00", + bumped: true, + bumped_at: "2014-01-13T11:38:15.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 2, + username: "neil", + avatar_template: "/images/avatar.png" + } + }, + { + id: 2544, + title: "Discourse central hub questions", + fancy_title: "Discourse central hub questions", + slug: "discourse-central-hub-questions", + posts_count: 51, + reply_count: 44, + highest_post_number: 52, + image_url: null, + created_at: "2013-02-09T04:28:21.000-05:00", + last_posted_at: "2013-09-19T13:36:49.000-04:00", + bumped: true, + bumped_at: "2013-09-19T14:04:08.000-04:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 2128, + username: "ultimape", + avatar_template: "/images/avatar.png" + } + }, + { + id: 424, + title: "What are the 'consequences' of changing your name?", + fancy_title: + "What are the ‘consequences’ of changing your name?", + slug: "what-are-the-consequences-of-changing-your-name", + posts_count: 35, + reply_count: 36, + highest_post_number: 43, + image_url: null, + created_at: "2013-02-05T17:37:52.000-05:00", + last_posted_at: "2013-09-19T13:55:11.000-04:00", + bumped: true, + bumped_at: "2013-09-19T13:55:11.000-04:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 2128, + username: "ultimape", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 13, + name: "blog", + color: "ED207B", + text_color: "FFFFFF", + slug: "blog", + topic_count: 14, + description: + "Discussion topics generated from the official Discourse Blog. These topics are linked from the bottom of each blog entry where the blog comments would normally be.", + topic_url: "/t/category-definition-for-blog/5250", + read_restricted: false, + permission: null, + post_count: 206, + topics_day: 0, + topics_week: 0, + topics_month: 1, + topics_year: 14, + posts_day: 0, + posts_week: 2, + posts_month: 11, + posts_year: 206, + description_excerpt: + "Discussion topics generated from the official Discourse Blog. These topics are linked from the bottom of each blog entry where the blog comments would normally be.", + featured_user_ids: [8134, 32, 4457, 4263, 1995], + topics: [ + { + id: 11835, + title: "The Road to Discourse 1.0", + fancy_title: "The Road to Discourse 1.0", + slug: "the-road-to-discourse-1-0", + posts_count: 6, + reply_count: 2, + highest_post_number: 6, + image_url: null, + created_at: "2014-01-08T19:08:44.000-05:00", + last_posted_at: "2014-01-16T04:49:16.000-05:00", + bumped: true, + bumped_at: "2014-01-16T04:49:16.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 8134, + username: "iontishina", + avatar_template: "/images/avatar.png" + } + }, + { + id: 5751, + title: "Discourse as Your First Rails App", + fancy_title: "Discourse as Your First Rails App", + slug: "discourse-as-your-first-rails-app", + posts_count: 62, + reply_count: 43, + highest_post_number: 71, + image_url: null, + created_at: "2013-04-09T19:08:33.000-04:00", + last_posted_at: "2013-12-19T18:27:37.000-05:00", + bumped: true, + bumped_at: "2013-12-19T18:27:37.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 1995, + username: "zogstrip", + avatar_template: "/images/avatar.png" + } + }, + { + id: 5898, + title: "The Discourse Servers", + fancy_title: "The Discourse Servers", + slug: "the-discourse-servers", + posts_count: 42, + reply_count: 32, + highest_post_number: 42, + image_url: null, + created_at: "2013-04-15T15:19:09.000-04:00", + last_posted_at: "2013-11-29T15:14:35.000-05:00", + bumped: true, + bumped_at: "2013-11-29T15:14:35.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6626, + username: "riking", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 4, + name: "faq", + color: "33b", + text_color: "FFFFFF", + slug: "faq", + topic_count: 49, + description: + "Topics that come up very often when discussing Discourse will eventually be classified into this Frequently Asked Questions category. Should only be added to popular topics.", + topic_url: "/t/category-definition-for-faq/25", + read_restricted: false, + permission: null, + post_count: 450, + topics_day: 0, + topics_week: 0, + topics_month: 0, + topics_year: 49, + posts_day: 0, + posts_week: 1, + posts_month: 10, + posts_year: 450, + description_excerpt: + "Topics that come up very often when discussing Discourse will eventually be classified into this Frequently Asked Questions category. Should only be added to popular topics.", + featured_user_ids: [32, 8047, 7483, 2, 6626], + topics: [ + { + id: 5372, + title: + "UX confusion (or me confusion) is it possible to edit old posts or only your most recent post in a topic?", + fancy_title: + "UX confusion (or me confusion) is it possible to edit old posts or only your most recent post in a topic?", + slug: + "ux-confusion-or-me-confusion-is-it-possible-to-edit-old-posts-or-only-your-most-recent-post-in-a-topic", + posts_count: 3, + reply_count: 0, + highest_post_number: 3, + image_url: null, + created_at: "2013-03-28T22:25:57.000-04:00", + last_posted_at: "2014-01-13T13:44:39.000-05:00", + bumped: true, + bumped_at: "2014-01-13T13:44:39.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + }, + { + id: 9631, + title: + "All the options to deploy Discourse with their relative pros and cons", + fancy_title: + "All the options to deploy Discourse with their relative pros and cons", + slug: + "all-the-options-to-deploy-discourse-with-their-relative-pros-and-cons", + posts_count: 14, + reply_count: 7, + highest_post_number: 15, + image_url: null, + created_at: "2013-09-06T03:55:09.000-04:00", + last_posted_at: "2013-09-26T18:49:04.000-04:00", + bumped: true, + bumped_at: "2013-12-30T12:32:59.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 3929, + username: "ScotterC", + avatar_template: "/images/avatar.png" + } + }, + { + id: 4325, + title: "How to delete a user?", + fancy_title: "How to delete a user?", + slug: "how-to-delete-a-user", + posts_count: 31, + reply_count: 23, + highest_post_number: 33, + image_url: null, + created_at: "2013-03-01T23:18:55.000-05:00", + last_posted_at: "2013-12-20T21:26:06.000-05:00", + bumped: true, + bumped_at: "2013-12-20T21:26:06.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 14, + name: "marketplace", + color: "8C6238", + text_color: "FFFFFF", + slug: "marketplace", + topic_count: 24, + description: + "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc.", + topic_url: "/t/category-definition-for-marketplace/5425", + read_restricted: false, + permission: null, + post_count: 106, + topics_day: 0, + topics_week: 1, + topics_month: 3, + topics_year: 24, + posts_day: 0, + posts_week: 1, + posts_month: 7, + posts_year: 106, + description_excerpt: + "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc.", + featured_user_ids: [6548, 32, 5548, 2291, 4755], + topics: [ + { + id: 11866, + title: "DiscourseHosting is now accepting BTC payments", + fancy_title: "DiscourseHosting is now accepting BTC payments", + slug: "discoursehosting-is-now-accepting-btc-payments", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-10T10:17:28.000-05:00", + last_posted_at: "2014-01-10T10:17:28.000-05:00", + bumped: false, + bumped_at: "2014-01-10T10:17:28.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6548, + username: "michaeld", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11571, + title: "Looking for a developer for Discourse Customization", + fancy_title: + "Looking for a developer for Discourse Customization", + slug: "looking-for-a-developer-for-discourse-customization", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: null, + created_at: "2013-12-23T20:54:04.000-05:00", + last_posted_at: "2013-12-24T13:12:17.000-05:00", + bumped: true, + bumped_at: "2013-12-30T16:36:17.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 2291, + username: "PabloC", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11594, + title: + "Need someone to fix a topic in my discourse install that won't load for moderators. Will pay", + fancy_title: + "Need someone to fix a topic in my discourse install that won’t load for moderators. Will pay", + slug: + "need-someone-to-fix-a-topic-in-my-discourse-install-that-wont-load-for-moderators-will-pay", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2013-12-25T10:25:57.000-05:00", + last_posted_at: "2013-12-26T17:01:41.000-05:00", + bumped: true, + bumped_at: "2013-12-25T17:01:15.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + last_poster: { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + } + } + ] + }, + { + id: 10, + name: "howto", + color: "76923C", + text_color: "FFFFFF", + slug: "howto", + topic_count: 58, + description: + "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up. ", + topic_url: "/t/category-definition-for-howto/2629", + read_restricted: false, + permission: null, + post_count: 677, + topics_day: 0, + topics_week: 0, + topics_month: 1, + topics_year: 58, + posts_day: 0, + posts_week: 0, + posts_month: 13, + posts_year: 675, + description_excerpt: + "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up.", + featured_user_ids: [7984, 4457, 1995, 6018, 5351], + topics: [ + { + id: 7582, + title: + "Twitter login with Passenger + Varnish - quick lessons learned", + fancy_title: + "Twitter login with Passenger + Varnish - quick lessons learned", + slug: + "twitter-login-with-passenger-varnish-quick-lessons-learned", + posts_count: 9, + reply_count: 3, + highest_post_number: 9, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2013-06-17T19:46:31.000-04:00", + last_posted_at: "2013-12-31T21:03:59.000-05:00", + bumped: true, + bumped_at: "2013-12-31T21:03:59.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 7984, + username: "sophearak", + avatar_template: "/images/avatar.png" + } + }, + { + id: 7229, + title: "How to set up image uploads to S3?", + fancy_title: "How to set up image uploads to S3?", + slug: "how-to-set-up-image-uploads-to-s3", + posts_count: 14, + reply_count: 11, + highest_post_number: 14, + image_url: "/uploads/meta_discourse/1019/782cbc7e309ce43f.png", + created_at: "2013-06-06T15:37:43.000-04:00", + last_posted_at: "2013-12-31T11:54:18.000-05:00", + bumped: true, + bumped_at: "2013-12-31T11:54:18.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 1995, + username: "zogstrip", + avatar_template: "/images/avatar.png" + } + }, + { + id: 11628, + title: + "My experience with a successful migration (hints for a guide)", + fancy_title: + "My experience with a successful migration (hints for a guide)", + slug: + "my-experience-with-a-successful-migration-hints-for-a-guide", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: null, + created_at: "2013-12-28T09:23:45.000-05:00", + last_posted_at: "2013-12-28T10:38:48.000-05:00", + bumped: true, + bumped_at: "2013-12-28T10:38:48.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + last_poster: { + id: 6018, + username: "robypez", + avatar_template: "/images/avatar.png" + } + } + ] + } + ] + } + }, + "/c/bug/l/latest.json": { + users: [ + { id: 1, username: "sam", avatar_template: "/images/avatar.png" }, + { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + }, + { + id: 8021, + username: "Abhishek_Gupta", + avatar_template: "/images/avatar.png" + }, + { + id: 6695, + username: "illspirit", + avatar_template: "/images/avatar.png" + }, + { id: 2, username: "neil", avatar_template: "/images/avatar.png" }, + { id: 3124, username: "sipp11", avatar_template: "/images/avatar.png" }, + { id: 7513, username: "digit", avatar_template: "/images/avatar.png" }, + { id: 19, username: "eviltrout", avatar_template: "/images/avatar.png" }, + { id: 3, username: "supermathie", avatar_template: "/images/avatar.png" }, + { id: 7073, username: "5an1ty", avatar_template: "/images/avatar.png" }, + { id: 4996, username: "wmertens", avatar_template: "/images/avatar.png" }, + { id: 6377, username: "zh99998", avatar_template: "/images/avatar.png" }, + { id: 1496, username: "cfstras", avatar_template: "/images/avatar.png" }, + { id: 7995, username: "Hunter", avatar_template: "/images/avatar.png" }, + { id: 6626, username: "riking", avatar_template: "/images/avatar.png" }, + { id: 1995, username: "zogstrip", avatar_template: "/images/avatar.png" }, + { + id: 5048, + username: "SneakySly", + avatar_template: "/images/avatar.png" + }, + { id: 7731, username: "YOU", avatar_template: "/images/avatar.png" }, + { + id: 7985, + username: "onlinedev", + avatar_template: "/images/avatar.png" + }, + { id: 3415, username: "radq", avatar_template: "/images/avatar.png" }, + { + id: 5351, + username: "erlend_sh", + avatar_template: "/images/avatar.png" + }, + { + id: 471, + username: "BhaelOchon", + avatar_template: "/images/avatar.png" + }, + { id: 7, username: "pekka", avatar_template: "/images/avatar.png" }, + { + id: 4780, + username: "HugoAlmeida", + avatar_template: "/images/avatar.png" + }, + { id: 5053, username: "Blue", avatar_template: "/images/avatar.png" }, + { id: 212, username: "alxndr", avatar_template: "/images/avatar.png" }, + { + id: 6118, + username: "lukelarris", + avatar_template: "/images/avatar.png" + }, + { + id: 7076, + username: "philnelson", + avatar_template: "/images/avatar.png" + }, + { id: 4851, username: "jab", avatar_template: "/images/avatar.png" }, + { id: 4457, username: "Lee_Ars", avatar_template: "/images/avatar.png" }, + { id: 6280, username: "mx2000", avatar_template: "/images/avatar.png" }, + { id: 3681, username: "Ajarn", avatar_template: "/images/avatar.png" }, + { id: 1621, username: "bnb", avatar_template: "/images/avatar.png" }, + { id: 6266, username: "bragi", avatar_template: "/images/avatar.png" }, + { id: 5335, username: "masda70", avatar_template: "/images/avatar.png" }, + { + id: 6314, + username: "rafaelfranca", + avatar_template: "/images/avatar.png" + } + ], + topic_list: { + can_create_topic: false, + more_topics_url: "/latest.json?category=1&page=1", + draft: null, + draft_key: "new_topic", + draft_sequence: null, + topics: [ + { + id: 2, + title: "Category definition for bug", + fancy_title: "Category definition for bug", + slug: "category-definition-for-bug", + posts_count: 2, + reply_count: 0, + highest_post_number: 3, + image_url: null, + created_at: "2013-01-31T23:56:34.000-05:00", + last_posted_at: "2013-03-07T22:42:27.000-05:00", + bumped: true, + bumped_at: "2013-02-26T18:52:56.000-05:00", + unseen: false, + pinned: true, + excerpt: + "Bug reports on Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", + visible: true, + closed: false, + archived: false, + views: 469, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11994, + title: "Cross domain rules, followed?", + fancy_title: "Cross domain rules, followed?", + slug: "cross-domain-rules-followed", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-16T09:59:15.000-05:00", + last_posted_at: "2014-01-16T09:59:15.000-05:00", + bumped: true, + bumped_at: "2014-01-16T11:04:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 15, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Abhishek_Gupta", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 8021 + } + ] + }, + { + id: 11888, + title: "Uncategorized topics not allowed, still seeing tag places", + fancy_title: + "Uncategorized topics not allowed, still seeing tag places", + slug: "uncategorized-topics-not-allowed-still-seeing-tag-places", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-10T19:23:37.000-05:00", + last_posted_at: "2014-01-15T22:41:25.000-05:00", + bumped: true, + bumped_at: "2014-01-15T22:41:25.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 50, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "illspirit", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6695 + }, + { extras: null, description: "Most Posts", user_id: 32 }, + { extras: null, description: "Frequent Poster", user_id: 2 } + ] + }, + { + id: 9151, + title: "Apple touch icon doesn't show if there is no sub domain", + fancy_title: + "Apple touch icon doesn’t show if there is no sub domain", + slug: "apple-touch-icon-doesnt-show-if-there-is-no-sub-domain", + posts_count: 7, + reply_count: 4, + highest_post_number: 7, + image_url: null, + created_at: "2013-08-16T18:16:53.000-04:00", + last_posted_at: "2014-01-15T17:10:18.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:19:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 188, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 3124 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 10911, + title: + "/users/activate-account pulling blank logo instead of defaulting to h2", + fancy_title: + "/users/activate-account pulling blank logo instead of defaulting to h2", + slug: + "users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: null, + created_at: "2013-11-12T14:49:04.000-05:00", + last_posted_at: "2014-01-15T10:21:37.000-05:00", + bumped: true, + bumped_at: "2014-01-15T10:21:37.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 121, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 7513 }, + { extras: "latest", description: "Most Recent Poster", user_id: 19 } + ] + }, + { + id: 11937, + title: "Smiley parser is busted", + fancy_title: "Smiley parser is busted", + slug: "smiley-parser-is-busted", + posts_count: 4, + reply_count: 4, + highest_post_number: 7, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-13T15:42:00.000-05:00", + last_posted_at: "2014-01-15T05:51:16.000-05:00", + bumped: true, + bumped_at: "2014-01-15T05:51:16.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 66, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 3 }, + { extras: null, description: "Most Posts", user_id: 7073 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 6625, + title: "Error 500 on PUT of site config", + fancy_title: "Error 500 on PUT of site config", + slug: "error-500-on-put-of-site-config", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2013-05-14T18:13:56.000-04:00", + last_posted_at: "2014-01-16T04:55:50.000-05:00", + bumped: true, + bumped_at: "2014-01-15T04:43:23.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 132, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 4996 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11225, + title: "Forum acts weirdly after client side updates", + fancy_title: "Forum acts weirdly after client side updates", + slug: "forum-acts-weirdly-after-client-side-updates", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2013-12-02T18:32:10.000-05:00", + last_posted_at: "2014-01-15T04:04:55.000-05:00", + bumped: true, + bumped_at: "2014-01-15T02:55:18.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 117, + like_count: 7, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11903, + title: "Error after update to 0.9.8.1", + fancy_title: "Error after update to 0.9.8.1", + slug: "error-after-update-to-0-9-8-1", + posts_count: 14, + reply_count: 6, + highest_post_number: 17, + image_url: null, + created_at: "2014-01-12T06:55:45.000-05:00", + last_posted_at: "2014-01-15T01:48:58.000-05:00", + bumped: true, + bumped_at: "2014-01-15T01:48:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 121, + like_count: 6, + has_summary: false, + archetype: "regular", + last_poster_username: "zh99998", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6377 + }, + { extras: null, description: "Most Posts", user_id: 1496 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 19 } + ] + }, + { + id: 11969, + title: "Qunit error and possibly related ember.js problem", + fancy_title: "Qunit error and possibly related ember.js problem", + slug: "qunit-error-and-possibly-related-ember-js-problem", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-14T22:51:32.000-05:00", + last_posted_at: "2014-01-14T22:51:32.000-05:00", + bumped: false, + bumped_at: "2014-01-14T22:51:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 32, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Hunter", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 7995 + } + ] + }, + { + id: 11945, + title: "Stuff disappears on the groups page", + fancy_title: "Stuff disappears on the groups page", + slug: "stuff-disappears-on-the-groups-page", + posts_count: 7, + reply_count: 2, + highest_post_number: 7, + image_url: null, + created_at: "2014-01-13T23:03:53.000-05:00", + last_posted_at: "2014-01-15T01:26:07.000-05:00", + bumped: true, + bumped_at: "2014-01-14T21:09:01.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 54, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 6695 }, + { extras: null, description: "Most Posts", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster, Frequent Poster", + user_id: 1995 + } + ] + }, + { + id: 11520, + title: "Discourse WordPress Plugin: Emoji's do not properly display", + fancy_title: + "Discourse WordPress Plugin: Emoji’s do not properly display", + slug: "discourse-wordpress-plugin-emojis-do-not-properly-display", + posts_count: 9, + reply_count: 4, + highest_post_number: 9, + image_url: + "/uploads/default/_optimized/638/4db/eff43a45b8_690x420.png", + created_at: "2013-12-19T23:32:03.000-05:00", + last_posted_at: "2014-01-15T04:32:19.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:53:34.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 168, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 5048 }, + { extras: null, description: "Frequent Poster", user_id: 7731 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11597, + title: + 'All categories drop down does not close after clicking on first menu "all categories"', + fancy_title: + "All categories drop down does not close after clicking on first menu “all categories”", + slug: + "all-categories-drop-down-does-not-close-after-clicking-on-first-menu-all-categories", + posts_count: 5, + reply_count: 2, + highest_post_number: 5, + image_url: "/uploads/default/2495/f9efe463ae67632d.png", + created_at: "2013-12-25T15:09:27.000-05:00", + last_posted_at: "2014-01-14T17:46:41.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:46:41.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 73, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "radq", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 7985 }, + { extras: null, description: "Most Posts", user_id: 32 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 3415 + } + ] + }, + { + id: 11962, + title: "Editor When Clicking on Wrench Issue", + fancy_title: "Editor When Clicking on Wrench Issue", + slug: "editor-when-clicking-on-wrench-issue", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: + "/uploads/default/_optimized/ca4/f70/ac7278b8f6_690x176.png", + created_at: "2014-01-14T17:23:20.000-05:00", + last_posted_at: "2014-01-14T17:24:02.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:24:02.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 30, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 7073 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11831, + title: "Broken links, possibly related to HTTPS", + fancy_title: "Broken links, possibly related to HTTPS", + slug: "broken-links-possibly-related-to-https", + posts_count: 17, + reply_count: 13, + highest_post_number: 18, + image_url: null, + created_at: "2014-01-08T17:40:45.000-05:00", + last_posted_at: "2014-01-14T16:03:07.000-05:00", + bumped: true, + bumped_at: "2014-01-14T16:03:07.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 102, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 5351 }, + { extras: null, description: "Most Posts", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 471 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 19 } + ] + }, + { + id: 11916, + title: "Unable to save user preferences", + fancy_title: "Unable to save user preferences", + slug: "unable-to-save-user-preferences", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2014-01-13T02:29:26.000-05:00", + last_posted_at: "2014-01-14T14:39:32.000-05:00", + bumped: true, + bumped_at: "2014-01-14T14:39:29.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 34, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 1995 + } + ] + }, + { + id: 10425, + title: "Editing category permissions: select value doesn't change", + fancy_title: + "Editing category permissions: select value doesn’t change", + slug: "editing-category-permissions-select-value-doesnt-change", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/uploads/meta_discourse/1956/d55fba29dbd7e1fe.png", + created_at: "2013-10-17T18:20:20.000-04:00", + last_posted_at: "2013-10-17T18:20:21.000-04:00", + bumped: true, + bumped_at: "2014-01-14T13:35:37.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 92, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "pekka", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 7 + } + ] + }, + { + id: 6557, + title: "Middle clicking a link twice does not work as expected", + fancy_title: "Middle clicking a link twice does not work as expected", + slug: "middle-clicking-a-link-twice-does-not-work-as-expected", + posts_count: 10, + reply_count: 7, + highest_post_number: 10, + image_url: null, + created_at: "2013-05-11T13:56:02.000-04:00", + last_posted_at: "2014-01-14T13:13:04.000-05:00", + bumped: true, + bumped_at: "2014-01-14T13:13:04.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 401, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "neil", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 4780 }, + { extras: null, description: "Most Posts", user_id: 5053 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 2 } + ] + }, + { + id: 11944, + title: "Regression: Cannot sort topic list", + fancy_title: "Regression: Cannot sort topic list", + slug: "regression-cannot-sort-topic-list", + posts_count: 5, + reply_count: 0, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-13T20:14:06.000-05:00", + last_posted_at: "2014-01-14T19:31:28.000-05:00", + bumped: true, + bumped_at: "2014-01-14T07:31:19.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: true, + views: 37, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 1995 + } + ] + }, + { + id: 10462, + title: "Rebake error when posts contain deleted YouTube video", + fancy_title: "Rebake error when posts contain deleted YouTube video", + slug: "rebake-error-when-posts-contain-deleted-youtube-video", + posts_count: 7, + reply_count: 1, + highest_post_number: 7, + image_url: null, + created_at: "2013-10-19T00:01:21.000-04:00", + last_posted_at: "2014-01-14T02:24:19.000-05:00", + bumped: true, + bumped_at: "2014-01-14T02:24:12.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 178, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 6695 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11932, + title: "Use of blockquote tag causes text outside a paragraph", + fancy_title: "Use of blockquote tag causes text outside a paragraph", + slug: "use-of-blockquote-tag-causes-text-outside-a-paragraph", + posts_count: 4, + reply_count: 2, + highest_post_number: 4, + image_url: null, + created_at: "2014-01-13T13:38:15.000-05:00", + last_posted_at: "2014-01-13T19:30:37.000-05:00", + bumped: true, + bumped_at: "2014-01-14T02:22:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 54, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 10357, + title: "Displaced Wrench Icon Chrome", + fancy_title: "Displaced Wrench Icon Chrome", + slug: "displaced-wrench-icon-chrome", + posts_count: 12, + reply_count: 4, + highest_post_number: 12, + image_url: + "/uploads/default/_optimized/9f3/f35/c5379beffe_690x300.jpg", + created_at: "2013-10-14T05:48:21.000-04:00", + last_posted_at: "2014-01-14T03:21:32.000-05:00", + bumped: true, + bumped_at: "2014-01-13T19:03:33.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 206, + like_count: 10, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 7073 }, + { extras: null, description: "Frequent Poster", user_id: 212 }, + { extras: null, description: "Frequent Poster", user_id: 6118 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 10114, + title: "Invitation expiry workflow is wonky", + fancy_title: "Invitation expiry workflow is wonky", + slug: "invitation-expiry-workflow-is-wonky", + posts_count: 14, + reply_count: 7, + highest_post_number: 14, + image_url: null, + created_at: "2013-09-30T00:59:36.000-04:00", + last_posted_at: "2014-01-13T18:51:26.000-05:00", + bumped: true, + bumped_at: "2014-01-13T18:51:26.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 176, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { extras: null, description: "Most Posts", user_id: 7076 }, + { extras: null, description: "Frequent Poster", user_id: 2 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 6330, + title: "Reply not disabled if topic closed while viewing", + fancy_title: "Reply not disabled if topic closed while viewing", + slug: "reply-not-disabled-if-topic-closed-while-viewing", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: + "https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon", + created_at: "2013-05-02T06:02:06.000-04:00", + last_posted_at: "2014-01-13T11:54:22.000-05:00", + bumped: true, + bumped_at: "2014-01-13T11:54:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 164, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 4851 }, + { extras: null, description: "Most Posts", user_id: 2 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 8367, + title: "Very fast scrolling fails to mark all posts read in a thread", + fancy_title: + "Very fast scrolling fails to mark all posts read in a thread", + slug: "very-fast-scrolling-fails-to-mark-all-posts-read-in-a-thread", + posts_count: 11, + reply_count: 7, + highest_post_number: 13, + image_url: null, + created_at: "2013-07-14T12:37:02.000-04:00", + last_posted_at: "2014-01-13T11:16:56.000-05:00", + bumped: true, + bumped_at: "2014-01-13T11:16:33.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 288, + like_count: 5, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 4457 }, + { extras: null, description: "Most Posts", user_id: 6280 }, + { extras: null, description: "Frequent Poster", user_id: 3681 }, + { extras: null, description: "Frequent Poster", user_id: 1621 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 8815, + title: "Cache headers confuse proxies", + fancy_title: "Cache headers confuse proxies", + slug: "cache-headers-confuse-proxies", + posts_count: 9, + reply_count: 3, + highest_post_number: 9, + image_url: null, + created_at: "2013-08-02T05:45:26.000-04:00", + last_posted_at: "2014-01-13T11:12:09.000-05:00", + bumped: true, + bumped_at: "2014-01-13T10:41:44.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 314, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 6266 }, + { extras: null, description: "Most Posts", user_id: 19 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 4457 }, + { + extras: "latest", + description: "Most Recent Poster, Frequent Poster", + user_id: 32 + } + ] + }, + { + id: 11371, + title: "Search not working for Staff users", + fancy_title: "Search not working for Staff users", + slug: "search-not-working-for-staff-users", + posts_count: 15, + reply_count: 10, + highest_post_number: 15, + image_url: null, + created_at: "2013-12-11T13:22:56.000-05:00", + last_posted_at: "2014-01-13T01:41:50.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:41:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 217, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 5335 }, + { extras: null, description: "Most Posts", user_id: 19 }, + { extras: null, description: "Frequent Poster", user_id: 6314 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 9908, + title: "Draft bar overrides pagination widget", + fancy_title: "Draft bar overrides pagination widget", + slug: "draft-bar-overrides-pagination-widget", + posts_count: 4, + reply_count: 0, + highest_post_number: 4, + image_url: null, + created_at: "2013-09-19T17:19:52.000-04:00", + last_posted_at: "2014-01-13T01:26:01.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:25:12.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 108, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 5351 }, + { extras: null, description: "Most Posts", user_id: 471 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 6134, + title: + "Unread topic is stuck as unread after insertion of staff message", + fancy_title: + "Unread topic is stuck as unread after insertion of staff message", + slug: + "unread-topic-is-stuck-as-unread-after-insertion-of-staff-message", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: + "https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon", + created_at: "2013-04-24T13:37:32.000-04:00", + last_posted_at: "2014-01-13T01:22:49.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:22:42.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 169, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 1, + posters: [ + { extras: null, description: "Original Poster", user_id: 3681 }, + { extras: null, description: "Most Posts", user_id: 5351 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11914, + title: "Google analytics is not registering page views", + fancy_title: "Google analytics is not registering page views", + slug: "google-analytics-is-not-registering-page-views", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-13T00:32:45.000-05:00", + last_posted_at: "2014-01-13T00:32:46.000-05:00", + bumped: true, + bumped_at: "2014-01-13T00:32:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 37, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 1, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 1 + } + ] + } + ] + } + }, + "/c/feature/l/latest.json": { + users: [ + { id: 1, username: "sam", avatar_template: "/images/avatar.png" }, + { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + }, + { + id: 8021, + username: "Abhishek_Gupta", + avatar_template: "/images/avatar.png" + }, + { + id: 6695, + username: "illspirit", + avatar_template: "/images/avatar.png" + }, + { id: 2, username: "neil", avatar_template: "/images/avatar.png" }, + { id: 3124, username: "sipp11", avatar_template: "/images/avatar.png" }, + { id: 7513, username: "digit", avatar_template: "/images/avatar.png" }, + { id: 19, username: "eviltrout", avatar_template: "/images/avatar.png" }, + { id: 3, username: "supermathie", avatar_template: "/images/avatar.png" }, + { id: 7073, username: "5an1ty", avatar_template: "/images/avatar.png" }, + { id: 4996, username: "wmertens", avatar_template: "/images/avatar.png" }, + { id: 6377, username: "zh99998", avatar_template: "/images/avatar.png" }, + { id: 1496, username: "cfstras", avatar_template: "/images/avatar.png" }, + { id: 7995, username: "Hunter", avatar_template: "/images/avatar.png" }, + { id: 6626, username: "riking", avatar_template: "/images/avatar.png" }, + { id: 1995, username: "zogstrip", avatar_template: "/images/avatar.png" }, + { + id: 5048, + username: "SneakySly", + avatar_template: "/images/avatar.png" + }, + { id: 7731, username: "YOU", avatar_template: "/images/avatar.png" }, + { + id: 7985, + username: "onlinedev", + avatar_template: "/images/avatar.png" + }, + { id: 3415, username: "radq", avatar_template: "/images/avatar.png" }, + { + id: 5351, + username: "erlend_sh", + avatar_template: "/images/avatar.png" + }, + { + id: 471, + username: "BhaelOchon", + avatar_template: "/images/avatar.png" + }, + { id: 7, username: "pekka", avatar_template: "/images/avatar.png" }, + { + id: 4780, + username: "HugoAlmeida", + avatar_template: "/images/avatar.png" + }, + { id: 5053, username: "Blue", avatar_template: "/images/avatar.png" }, + { id: 212, username: "alxndr", avatar_template: "/images/avatar.png" }, + { + id: 6118, + username: "lukelarris", + avatar_template: "/images/avatar.png" + }, + { + id: 7076, + username: "philnelson", + avatar_template: "/images/avatar.png" + }, + { id: 4851, username: "jab", avatar_template: "/images/avatar.png" }, + { id: 4457, username: "Lee_Ars", avatar_template: "/images/avatar.png" }, + { id: 6280, username: "mx2000", avatar_template: "/images/avatar.png" }, + { id: 3681, username: "Ajarn", avatar_template: "/images/avatar.png" }, + { id: 1621, username: "bnb", avatar_template: "/images/avatar.png" }, + { id: 6266, username: "bragi", avatar_template: "/images/avatar.png" }, + { id: 5335, username: "masda70", avatar_template: "/images/avatar.png" }, + { + id: 6314, + username: "rafaelfranca", + avatar_template: "/images/avatar.png" + } + ], + topic_list: { + can_create_topic: false, + more_topics_url: "/latest.json?category=2&page=1", + draft: null, + draft_key: "new_topic", + draft_sequence: null, + topics: [ + { + id: 2, + title: "Category definition for feature", + fancy_title: "Category definition for feature", + slug: "category-definition-for-feature", + posts_count: 2, + reply_count: 0, + highest_post_number: 3, + image_url: null, + created_at: "2013-01-31T23:56:34.000-05:00", + last_posted_at: "2013-03-07T22:42:27.000-05:00", + bumped: true, + bumped_at: "2013-02-26T18:52:56.000-05:00", + unseen: false, + pinned: true, + excerpt: "Features on Discourse.", + visible: true, + closed: false, + archived: false, + views: 469, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11994, + title: "Cross domain rules, followed?", + fancy_title: "Cross domain rules, followed?", + slug: "cross-domain-rules-followed", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-16T09:59:15.000-05:00", + last_posted_at: "2014-01-16T09:59:15.000-05:00", + bumped: true, + bumped_at: "2014-01-16T11:04:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 15, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Abhishek_Gupta", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 8021 + } + ] + }, + { + id: 11888, + title: "Uncategorized topics not allowed, still seeing tag places", + fancy_title: + "Uncategorized topics not allowed, still seeing tag places", + slug: "uncategorized-topics-not-allowed-still-seeing-tag-places", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-10T19:23:37.000-05:00", + last_posted_at: "2014-01-15T22:41:25.000-05:00", + bumped: true, + bumped_at: "2014-01-15T22:41:25.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 50, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "illspirit", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6695 + }, + { extras: null, description: "Most Posts", user_id: 32 }, + { extras: null, description: "Frequent Poster", user_id: 2 } + ] + }, + { + id: 9151, + title: "Apple touch icon doesn't show if there is no sub domain", + fancy_title: + "Apple touch icon doesn’t show if there is no sub domain", + slug: "apple-touch-icon-doesnt-show-if-there-is-no-sub-domain", + posts_count: 7, + reply_count: 4, + highest_post_number: 7, + image_url: null, + created_at: "2013-08-16T18:16:53.000-04:00", + last_posted_at: "2014-01-15T17:10:18.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:19:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 188, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 3124 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 10911, + title: + "/users/activate-account pulling blank logo instead of defaulting to h2", + fancy_title: + "/users/activate-account pulling blank logo instead of defaulting to h2", + slug: + "users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: null, + created_at: "2013-11-12T14:49:04.000-05:00", + last_posted_at: "2014-01-15T10:21:37.000-05:00", + bumped: true, + bumped_at: "2014-01-15T10:21:37.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 121, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 7513 }, + { extras: "latest", description: "Most Recent Poster", user_id: 19 } + ] + }, + { + id: 11937, + title: "Smiley parser is busted", + fancy_title: "Smiley parser is busted", + slug: "smiley-parser-is-busted", + posts_count: 4, + reply_count: 4, + highest_post_number: 7, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-13T15:42:00.000-05:00", + last_posted_at: "2014-01-15T05:51:16.000-05:00", + bumped: true, + bumped_at: "2014-01-15T05:51:16.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 66, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 3 }, + { extras: null, description: "Most Posts", user_id: 7073 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 6625, + title: "Error 500 on PUT of site config", + fancy_title: "Error 500 on PUT of site config", + slug: "error-500-on-put-of-site-config", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2013-05-14T18:13:56.000-04:00", + last_posted_at: "2014-01-16T04:55:50.000-05:00", + bumped: true, + bumped_at: "2014-01-15T04:43:23.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 132, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 4996 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11225, + title: "Forum acts weirdly after client side updates", + fancy_title: "Forum acts weirdly after client side updates", + slug: "forum-acts-weirdly-after-client-side-updates", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2013-12-02T18:32:10.000-05:00", + last_posted_at: "2014-01-15T04:04:55.000-05:00", + bumped: true, + bumped_at: "2014-01-15T02:55:18.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 117, + like_count: 7, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11903, + title: "Error after update to 0.9.8.1", + fancy_title: "Error after update to 0.9.8.1", + slug: "error-after-update-to-0-9-8-1", + posts_count: 14, + reply_count: 6, + highest_post_number: 17, + image_url: null, + created_at: "2014-01-12T06:55:45.000-05:00", + last_posted_at: "2014-01-15T01:48:58.000-05:00", + bumped: true, + bumped_at: "2014-01-15T01:48:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 121, + like_count: 6, + has_summary: false, + archetype: "regular", + last_poster_username: "zh99998", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6377 + }, + { extras: null, description: "Most Posts", user_id: 1496 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 19 } + ] + }, + { + id: 11969, + title: "Qunit error and possibly related ember.js problem", + fancy_title: "Qunit error and possibly related ember.js problem", + slug: "qunit-error-and-possibly-related-ember-js-problem", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-14T22:51:32.000-05:00", + last_posted_at: "2014-01-14T22:51:32.000-05:00", + bumped: false, + bumped_at: "2014-01-14T22:51:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 32, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Hunter", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 7995 + } + ] + }, + { + id: 11945, + title: "Stuff disappears on the groups page", + fancy_title: "Stuff disappears on the groups page", + slug: "stuff-disappears-on-the-groups-page", + posts_count: 7, + reply_count: 2, + highest_post_number: 7, + image_url: null, + created_at: "2014-01-13T23:03:53.000-05:00", + last_posted_at: "2014-01-15T01:26:07.000-05:00", + bumped: true, + bumped_at: "2014-01-14T21:09:01.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 54, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6695 }, + { extras: null, description: "Most Posts", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster, Frequent Poster", + user_id: 1995 + } + ] + }, + { + id: 11520, + title: "Discourse WordPress Plugin: Emoji's do not properly display", + fancy_title: + "Discourse WordPress Plugin: Emoji’s do not properly display", + slug: "discourse-wordpress-plugin-emojis-do-not-properly-display", + posts_count: 9, + reply_count: 4, + highest_post_number: 9, + image_url: + "/uploads/default/_optimized/638/4db/eff43a45b8_690x420.png", + created_at: "2013-12-19T23:32:03.000-05:00", + last_posted_at: "2014-01-15T04:32:19.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:53:34.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 168, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 5048 }, + { extras: null, description: "Frequent Poster", user_id: 7731 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11597, + title: + 'All categories drop down does not close after clicking on first menu "all categories"', + fancy_title: + "All categories drop down does not close after clicking on first menu “all categories”", + slug: + "all-categories-drop-down-does-not-close-after-clicking-on-first-menu-all-categories", + posts_count: 5, + reply_count: 2, + highest_post_number: 5, + image_url: "/uploads/default/2495/f9efe463ae67632d.png", + created_at: "2013-12-25T15:09:27.000-05:00", + last_posted_at: "2014-01-14T17:46:41.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:46:41.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 73, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "radq", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 7985 }, + { extras: null, description: "Most Posts", user_id: 32 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 3415 + } + ] + }, + { + id: 11962, + title: "Editor When Clicking on Wrench Issue", + fancy_title: "Editor When Clicking on Wrench Issue", + slug: "editor-when-clicking-on-wrench-issue", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: + "/uploads/default/_optimized/ca4/f70/ac7278b8f6_690x176.png", + created_at: "2014-01-14T17:23:20.000-05:00", + last_posted_at: "2014-01-14T17:24:02.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:24:02.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 30, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 7073 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11831, + title: "Broken links, possibly related to HTTPS", + fancy_title: "Broken links, possibly related to HTTPS", + slug: "broken-links-possibly-related-to-https", + posts_count: 17, + reply_count: 13, + highest_post_number: 18, + image_url: null, + created_at: "2014-01-08T17:40:45.000-05:00", + last_posted_at: "2014-01-14T16:03:07.000-05:00", + bumped: true, + bumped_at: "2014-01-14T16:03:07.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 102, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 5351 }, + { extras: null, description: "Most Posts", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 471 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 19 } + ] + }, + { + id: 11916, + title: "Unable to save user preferences", + fancy_title: "Unable to save user preferences", + slug: "unable-to-save-user-preferences", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2014-01-13T02:29:26.000-05:00", + last_posted_at: "2014-01-14T14:39:32.000-05:00", + bumped: true, + bumped_at: "2014-01-14T14:39:29.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 34, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 1995 + } + ] + }, + { + id: 10425, + title: "Editing category permissions: select value doesn't change", + fancy_title: + "Editing category permissions: select value doesn’t change", + slug: "editing-category-permissions-select-value-doesnt-change", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/uploads/meta_discourse/1956/d55fba29dbd7e1fe.png", + created_at: "2013-10-17T18:20:20.000-04:00", + last_posted_at: "2013-10-17T18:20:21.000-04:00", + bumped: true, + bumped_at: "2014-01-14T13:35:37.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 92, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "pekka", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 7 + } + ] + }, + { + id: 6557, + title: "Middle clicking a link twice does not work as expected", + fancy_title: "Middle clicking a link twice does not work as expected", + slug: "middle-clicking-a-link-twice-does-not-work-as-expected", + posts_count: 10, + reply_count: 7, + highest_post_number: 10, + image_url: null, + created_at: "2013-05-11T13:56:02.000-04:00", + last_posted_at: "2014-01-14T13:13:04.000-05:00", + bumped: true, + bumped_at: "2014-01-14T13:13:04.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 401, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "neil", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 4780 }, + { extras: null, description: "Most Posts", user_id: 5053 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 2 } + ] + }, + { + id: 11944, + title: "Regression: Cannot sort topic list", + fancy_title: "Regression: Cannot sort topic list", + slug: "regression-cannot-sort-topic-list", + posts_count: 5, + reply_count: 0, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-13T20:14:06.000-05:00", + last_posted_at: "2014-01-14T19:31:28.000-05:00", + bumped: true, + bumped_at: "2014-01-14T07:31:19.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: true, + views: 37, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 1995 + } + ] + }, + { + id: 10462, + title: "Rebake error when posts contain deleted YouTube video", + fancy_title: "Rebake error when posts contain deleted YouTube video", + slug: "rebake-error-when-posts-contain-deleted-youtube-video", + posts_count: 7, + reply_count: 1, + highest_post_number: 7, + image_url: null, + created_at: "2013-10-19T00:01:21.000-04:00", + last_posted_at: "2014-01-14T02:24:19.000-05:00", + bumped: true, + bumped_at: "2014-01-14T02:24:12.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 178, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6695 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11932, + title: "Use of blockquote tag causes text outside a paragraph", + fancy_title: "Use of blockquote tag causes text outside a paragraph", + slug: "use-of-blockquote-tag-causes-text-outside-a-paragraph", + posts_count: 4, + reply_count: 2, + highest_post_number: 4, + image_url: null, + created_at: "2014-01-13T13:38:15.000-05:00", + last_posted_at: "2014-01-13T19:30:37.000-05:00", + bumped: true, + bumped_at: "2014-01-14T02:22:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 54, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 10357, + title: "Displaced Wrench Icon Chrome", + fancy_title: "Displaced Wrench Icon Chrome", + slug: "displaced-wrench-icon-chrome", + posts_count: 12, + reply_count: 4, + highest_post_number: 12, + image_url: + "/uploads/default/_optimized/9f3/f35/c5379beffe_690x300.jpg", + created_at: "2013-10-14T05:48:21.000-04:00", + last_posted_at: "2014-01-14T03:21:32.000-05:00", + bumped: true, + bumped_at: "2014-01-13T19:03:33.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 206, + like_count: 10, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 7073 }, + { extras: null, description: "Frequent Poster", user_id: 212 }, + { extras: null, description: "Frequent Poster", user_id: 6118 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 10114, + title: "Invitation expiry workflow is wonky", + fancy_title: "Invitation expiry workflow is wonky", + slug: "invitation-expiry-workflow-is-wonky", + posts_count: 14, + reply_count: 7, + highest_post_number: 14, + image_url: null, + created_at: "2013-09-30T00:59:36.000-04:00", + last_posted_at: "2014-01-13T18:51:26.000-05:00", + bumped: true, + bumped_at: "2014-01-13T18:51:26.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 176, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { extras: null, description: "Most Posts", user_id: 7076 }, + { extras: null, description: "Frequent Poster", user_id: 2 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 6330, + title: "Reply not disabled if topic closed while viewing", + fancy_title: "Reply not disabled if topic closed while viewing", + slug: "reply-not-disabled-if-topic-closed-while-viewing", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: + "https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon", + created_at: "2013-05-02T06:02:06.000-04:00", + last_posted_at: "2014-01-13T11:54:22.000-05:00", + bumped: true, + bumped_at: "2014-01-13T11:54:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 164, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 4851 }, + { extras: null, description: "Most Posts", user_id: 2 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 8367, + title: "Very fast scrolling fails to mark all posts read in a thread", + fancy_title: + "Very fast scrolling fails to mark all posts read in a thread", + slug: "very-fast-scrolling-fails-to-mark-all-posts-read-in-a-thread", + posts_count: 11, + reply_count: 7, + highest_post_number: 13, + image_url: null, + created_at: "2013-07-14T12:37:02.000-04:00", + last_posted_at: "2014-01-13T11:16:56.000-05:00", + bumped: true, + bumped_at: "2014-01-13T11:16:33.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 288, + like_count: 5, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 4457 }, + { extras: null, description: "Most Posts", user_id: 6280 }, + { extras: null, description: "Frequent Poster", user_id: 3681 }, + { extras: null, description: "Frequent Poster", user_id: 1621 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 8815, + title: "Cache headers confuse proxies", + fancy_title: "Cache headers confuse proxies", + slug: "cache-headers-confuse-proxies", + posts_count: 9, + reply_count: 3, + highest_post_number: 9, + image_url: null, + created_at: "2013-08-02T05:45:26.000-04:00", + last_posted_at: "2014-01-13T11:12:09.000-05:00", + bumped: true, + bumped_at: "2014-01-13T10:41:44.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 314, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 6266 }, + { extras: null, description: "Most Posts", user_id: 19 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 4457 }, + { + extras: "latest", + description: "Most Recent Poster, Frequent Poster", + user_id: 32 + } + ] + }, + { + id: 11371, + title: "Search not working for Staff users", + fancy_title: "Search not working for Staff users", + slug: "search-not-working-for-staff-users", + posts_count: 15, + reply_count: 10, + highest_post_number: 15, + image_url: null, + created_at: "2013-12-11T13:22:56.000-05:00", + last_posted_at: "2014-01-13T01:41:50.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:41:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 217, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 5335 }, + { extras: null, description: "Most Posts", user_id: 19 }, + { extras: null, description: "Frequent Poster", user_id: 6314 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 9908, + title: "Draft bar overrides pagination widget", + fancy_title: "Draft bar overrides pagination widget", + slug: "draft-bar-overrides-pagination-widget", + posts_count: 4, + reply_count: 0, + highest_post_number: 4, + image_url: null, + created_at: "2013-09-19T17:19:52.000-04:00", + last_posted_at: "2014-01-13T01:26:01.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:25:12.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 108, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 5351 }, + { extras: null, description: "Most Posts", user_id: 471 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 6134, + title: + "Unread topic is stuck as unread after insertion of staff message", + fancy_title: + "Unread topic is stuck as unread after insertion of staff message", + slug: + "unread-topic-is-stuck-as-unread-after-insertion-of-staff-message", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: + "https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon", + created_at: "2013-04-24T13:37:32.000-04:00", + last_posted_at: "2014-01-13T01:22:49.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:22:42.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 169, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 2, + posters: [ + { extras: null, description: "Original Poster", user_id: 3681 }, + { extras: null, description: "Most Posts", user_id: 5351 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11914, + title: "Google analytics is not registering page views", + fancy_title: "Google analytics is not registering page views", + slug: "google-analytics-is-not-registering-page-views", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-13T00:32:45.000-05:00", + last_posted_at: "2014-01-13T00:32:46.000-05:00", + bumped: true, + bumped_at: "2014-01-13T00:32:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 37, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 2, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 1 + } + ] + } + ] + } + }, + "/c/dev/l/latest.json": { + users: [ + { id: 1, username: "sam", avatar_template: "/images/avatar.png" }, + { + id: 32, + username: "codinghorror", + avatar_template: "/images/avatar.png" + }, + { + id: 8021, + username: "Abhishek_Gupta", + avatar_template: "/images/avatar.png" + }, + { + id: 6695, + username: "illspirit", + avatar_template: "/images/avatar.png" + }, + { id: 2, username: "neil", avatar_template: "/images/avatar.png" }, + { id: 3124, username: "sipp11", avatar_template: "/images/avatar.png" }, + { id: 7513, username: "digit", avatar_template: "/images/avatar.png" }, + { id: 19, username: "eviltrout", avatar_template: "/images/avatar.png" }, + { id: 3, username: "supermathie", avatar_template: "/images/avatar.png" }, + { id: 7073, username: "5an1ty", avatar_template: "/images/avatar.png" }, + { id: 4996, username: "wmertens", avatar_template: "/images/avatar.png" }, + { id: 6377, username: "zh99998", avatar_template: "/images/avatar.png" }, + { id: 1496, username: "cfstras", avatar_template: "/images/avatar.png" }, + { id: 7995, username: "Hunter", avatar_template: "/images/avatar.png" }, + { id: 6626, username: "riking", avatar_template: "/images/avatar.png" }, + { id: 1995, username: "zogstrip", avatar_template: "/images/avatar.png" }, + { + id: 5048, + username: "SneakySly", + avatar_template: "/images/avatar.png" + }, + { id: 7731, username: "YOU", avatar_template: "/images/avatar.png" }, + { + id: 7985, + username: "onlinedev", + avatar_template: "/images/avatar.png" + }, + { id: 3415, username: "radq", avatar_template: "/images/avatar.png" }, + { + id: 5351, + username: "erlend_sh", + avatar_template: "/images/avatar.png" + }, + { + id: 471, + username: "BhaelOchon", + avatar_template: "/images/avatar.png" + }, + { id: 7, username: "pekka", avatar_template: "/images/avatar.png" }, + { + id: 4780, + username: "HugoAlmeida", + avatar_template: "/images/avatar.png" + }, + { id: 5053, username: "Blue", avatar_template: "/images/avatar.png" }, + { id: 212, username: "alxndr", avatar_template: "/images/avatar.png" }, + { + id: 6118, + username: "lukelarris", + avatar_template: "/images/avatar.png" + }, + { + id: 7076, + username: "philnelson", + avatar_template: "/images/avatar.png" + }, + { id: 4851, username: "jab", avatar_template: "/images/avatar.png" }, + { id: 4457, username: "Lee_Ars", avatar_template: "/images/avatar.png" }, + { id: 6280, username: "mx2000", avatar_template: "/images/avatar.png" }, + { id: 3681, username: "Ajarn", avatar_template: "/images/avatar.png" }, + { id: 1621, username: "bnb", avatar_template: "/images/avatar.png" }, + { id: 6266, username: "bragi", avatar_template: "/images/avatar.png" }, + { id: 5335, username: "masda70", avatar_template: "/images/avatar.png" }, + { + id: 6314, + username: "rafaelfranca", + avatar_template: "/images/avatar.png" + } + ], + topic_list: { + can_create_topic: false, + more_topics_url: "/latest.json?category=2&page=1", + draft: null, + draft_key: "new_topic", + draft_sequence: null, + topics: [ + { + id: 2, + title: "Category definition for dev", + fancy_title: "Category definition for dev", + slug: "category-definition-for-dev", + posts_count: 2, + reply_count: 0, + highest_post_number: 3, + image_url: null, + created_at: "2013-01-31T23:56:34.000-05:00", + last_posted_at: "2013-03-07T22:42:27.000-05:00", + bumped: true, + bumped_at: "2013-02-26T18:52:56.000-05:00", + unseen: false, + pinned: true, + excerpt: "Development of Discourse.", + visible: true, + closed: false, + archived: false, + views: 469, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11994, + title: "Cross domain rules, followed?", + fancy_title: "Cross domain rules, followed?", + slug: "cross-domain-rules-followed", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-16T09:59:15.000-05:00", + last_posted_at: "2014-01-16T09:59:15.000-05:00", + bumped: true, + bumped_at: "2014-01-16T11:04:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 15, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Abhishek_Gupta", + category_id: 7, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 8021 + } + ] + }, + { + id: 11888, + title: "Uncategorized topics not allowed, still seeing tag places", + fancy_title: + "Uncategorized topics not allowed, still seeing tag places", + slug: "uncategorized-topics-not-allowed-still-seeing-tag-places", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-10T19:23:37.000-05:00", + last_posted_at: "2014-01-15T22:41:25.000-05:00", + bumped: true, + bumped_at: "2014-01-15T22:41:25.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 50, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "illspirit", + category_id: 7, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6695 + }, + { extras: null, description: "Most Posts", user_id: 32 }, + { extras: null, description: "Frequent Poster", user_id: 2 } + ] + }, + { + id: 9151, + title: "Apple touch icon doesn't show if there is no sub domain", + fancy_title: + "Apple touch icon doesn’t show if there is no sub domain", + slug: "apple-touch-icon-doesnt-show-if-there-is-no-sub-domain", + posts_count: 7, + reply_count: 4, + highest_post_number: 7, + image_url: null, + created_at: "2013-08-16T18:16:53.000-04:00", + last_posted_at: "2014-01-15T17:10:18.000-05:00", + bumped: true, + bumped_at: "2014-01-15T13:19:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 188, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 3124 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 10911, + title: + "/users/activate-account pulling blank logo instead of defaulting to h2", + fancy_title: + "/users/activate-account pulling blank logo instead of defaulting to h2", + slug: + "users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: null, + created_at: "2013-11-12T14:49:04.000-05:00", + last_posted_at: "2014-01-15T10:21:37.000-05:00", + bumped: true, + bumped_at: "2014-01-15T10:21:37.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 121, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 7513 }, + { extras: "latest", description: "Most Recent Poster", user_id: 19 } + ] + }, + { + id: 11937, + title: "Smiley parser is busted", + fancy_title: "Smiley parser is busted", + slug: "smiley-parser-is-busted", + posts_count: 4, + reply_count: 4, + highest_post_number: 7, + image_url: "/plugins/emoji/images/smile.png", + created_at: "2014-01-13T15:42:00.000-05:00", + last_posted_at: "2014-01-15T05:51:16.000-05:00", + bumped: true, + bumped_at: "2014-01-15T05:51:16.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 66, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 3 }, + { extras: null, description: "Most Posts", user_id: 7073 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 6625, + title: "Error 500 on PUT of site config", + fancy_title: "Error 500 on PUT of site config", + slug: "error-500-on-put-of-site-config", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2013-05-14T18:13:56.000-04:00", + last_posted_at: "2014-01-16T04:55:50.000-05:00", + bumped: true, + bumped_at: "2014-01-15T04:43:23.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 132, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 4996 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11225, + title: "Forum acts weirdly after client side updates", + fancy_title: "Forum acts weirdly after client side updates", + slug: "forum-acts-weirdly-after-client-side-updates", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: null, + created_at: "2013-12-02T18:32:10.000-05:00", + last_posted_at: "2014-01-15T04:04:55.000-05:00", + bumped: true, + bumped_at: "2014-01-15T02:55:18.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 117, + like_count: 7, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11903, + title: "Error after update to 0.9.8.1", + fancy_title: "Error after update to 0.9.8.1", + slug: "error-after-update-to-0-9-8-1", + posts_count: 14, + reply_count: 6, + highest_post_number: 17, + image_url: null, + created_at: "2014-01-12T06:55:45.000-05:00", + last_posted_at: "2014-01-15T01:48:58.000-05:00", + bumped: true, + bumped_at: "2014-01-15T01:48:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 121, + like_count: 6, + has_summary: false, + archetype: "regular", + last_poster_username: "zh99998", + category_id: 7, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 6377 + }, + { extras: null, description: "Most Posts", user_id: 1496 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 19 } + ] + }, + { + id: 11969, + title: "Qunit error and possibly related ember.js problem", + fancy_title: "Qunit error and possibly related ember.js problem", + slug: "qunit-error-and-possibly-related-ember-js-problem", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-14T22:51:32.000-05:00", + last_posted_at: "2014-01-14T22:51:32.000-05:00", + bumped: false, + bumped_at: "2014-01-14T22:51:32.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 32, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "Hunter", + category_id: 7, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 7995 + } + ] + }, + { + id: 11945, + title: "Stuff disappears on the groups page", + fancy_title: "Stuff disappears on the groups page", + slug: "stuff-disappears-on-the-groups-page", + posts_count: 7, + reply_count: 2, + highest_post_number: 7, + image_url: null, + created_at: "2014-01-13T23:03:53.000-05:00", + last_posted_at: "2014-01-15T01:26:07.000-05:00", + bumped: true, + bumped_at: "2014-01-14T21:09:01.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 54, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 6695 }, + { extras: null, description: "Most Posts", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster, Frequent Poster", + user_id: 1995 + } + ] + }, + { + id: 11520, + title: "Discourse WordPress Plugin: Emoji's do not properly display", + fancy_title: + "Discourse WordPress Plugin: Emoji’s do not properly display", + slug: "discourse-wordpress-plugin-emojis-do-not-properly-display", + posts_count: 9, + reply_count: 4, + highest_post_number: 9, + image_url: + "/uploads/default/_optimized/638/4db/eff43a45b8_690x420.png", + created_at: "2013-12-19T23:32:03.000-05:00", + last_posted_at: "2014-01-15T04:32:19.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:53:34.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 168, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 5048 }, + { extras: null, description: "Frequent Poster", user_id: 7731 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 11597, + title: + 'All categories drop down does not close after clicking on first menu "all categories"', + fancy_title: + "All categories drop down does not close after clicking on first menu “all categories”", + slug: + "all-categories-drop-down-does-not-close-after-clicking-on-first-menu-all-categories", + posts_count: 5, + reply_count: 2, + highest_post_number: 5, + image_url: "/uploads/default/2495/f9efe463ae67632d.png", + created_at: "2013-12-25T15:09:27.000-05:00", + last_posted_at: "2014-01-14T17:46:41.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:46:41.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 73, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "radq", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 7985 }, + { extras: null, description: "Most Posts", user_id: 32 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 3415 + } + ] + }, + { + id: 11962, + title: "Editor When Clicking on Wrench Issue", + fancy_title: "Editor When Clicking on Wrench Issue", + slug: "editor-when-clicking-on-wrench-issue", + posts_count: 2, + reply_count: 0, + highest_post_number: 2, + image_url: + "/uploads/default/_optimized/ca4/f70/ac7278b8f6_690x176.png", + created_at: "2014-01-14T17:23:20.000-05:00", + last_posted_at: "2014-01-14T17:24:02.000-05:00", + bumped: true, + bumped_at: "2014-01-14T17:24:02.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 30, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 7073 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11831, + title: "Broken links, possibly related to HTTPS", + fancy_title: "Broken links, possibly related to HTTPS", + slug: "broken-links-possibly-related-to-https", + posts_count: 17, + reply_count: 13, + highest_post_number: 18, + image_url: null, + created_at: "2014-01-08T17:40:45.000-05:00", + last_posted_at: "2014-01-14T16:03:07.000-05:00", + bumped: true, + bumped_at: "2014-01-14T16:03:07.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 102, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 5351 }, + { extras: null, description: "Most Posts", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 471 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 19 } + ] + }, + { + id: 11916, + title: "Unable to save user preferences", + fancy_title: "Unable to save user preferences", + slug: "unable-to-save-user-preferences", + posts_count: 4, + reply_count: 1, + highest_post_number: 4, + image_url: null, + created_at: "2014-01-13T02:29:26.000-05:00", + last_posted_at: "2014-01-14T14:39:32.000-05:00", + bumped: true, + bumped_at: "2014-01-14T14:39:29.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 34, + like_count: 3, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster", + user_id: 1995 + } + ] + }, + { + id: 10425, + title: "Editing category permissions: select value doesn't change", + fancy_title: + "Editing category permissions: select value doesn’t change", + slug: "editing-category-permissions-select-value-doesnt-change", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: "/uploads/meta_discourse/1956/d55fba29dbd7e1fe.png", + created_at: "2013-10-17T18:20:20.000-04:00", + last_posted_at: "2013-10-17T18:20:21.000-04:00", + bumped: true, + bumped_at: "2014-01-14T13:35:37.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 92, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "pekka", + category_id: 7, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 7 + } + ] + }, + { + id: 6557, + title: "Middle clicking a link twice does not work as expected", + fancy_title: "Middle clicking a link twice does not work as expected", + slug: "middle-clicking-a-link-twice-does-not-work-as-expected", + posts_count: 10, + reply_count: 7, + highest_post_number: 10, + image_url: null, + created_at: "2013-05-11T13:56:02.000-04:00", + last_posted_at: "2014-01-14T13:13:04.000-05:00", + bumped: true, + bumped_at: "2014-01-14T13:13:04.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 401, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "neil", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 4780 }, + { extras: null, description: "Most Posts", user_id: 5053 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 2 } + ] + }, + { + id: 11944, + title: "Regression: Cannot sort topic list", + fancy_title: "Regression: Cannot sort topic list", + slug: "regression-cannot-sort-topic-list", + posts_count: 5, + reply_count: 0, + highest_post_number: 5, + image_url: null, + created_at: "2014-01-13T20:14:06.000-05:00", + last_posted_at: "2014-01-14T19:31:28.000-05:00", + bumped: true, + bumped_at: "2014-01-14T07:31:19.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: true, + views: 37, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "zogstrip", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 1995 + } + ] + }, + { + id: 10462, + title: "Rebake error when posts contain deleted YouTube video", + fancy_title: "Rebake error when posts contain deleted YouTube video", + slug: "rebake-error-when-posts-contain-deleted-youtube-video", + posts_count: 7, + reply_count: 1, + highest_post_number: 7, + image_url: null, + created_at: "2013-10-19T00:01:21.000-04:00", + last_posted_at: "2014-01-14T02:24:19.000-05:00", + bumped: true, + bumped_at: "2014-01-14T02:24:12.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 178, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 6695 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11932, + title: "Use of blockquote tag causes text outside a paragraph", + fancy_title: "Use of blockquote tag causes text outside a paragraph", + slug: "use-of-blockquote-tag-causes-text-outside-a-paragraph", + posts_count: 4, + reply_count: 2, + highest_post_number: 4, + image_url: null, + created_at: "2014-01-13T13:38:15.000-05:00", + last_posted_at: "2014-01-13T19:30:37.000-05:00", + bumped: true, + bumped_at: "2014-01-14T02:22:58.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 54, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 6626 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 10357, + title: "Displaced Wrench Icon Chrome", + fancy_title: "Displaced Wrench Icon Chrome", + slug: "displaced-wrench-icon-chrome", + posts_count: 12, + reply_count: 4, + highest_post_number: 12, + image_url: + "/uploads/default/_optimized/9f3/f35/c5379beffe_690x300.jpg", + created_at: "2013-10-14T05:48:21.000-04:00", + last_posted_at: "2014-01-14T03:21:32.000-05:00", + bumped: true, + bumped_at: "2014-01-13T19:03:33.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 206, + like_count: 10, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 7073 }, + { extras: null, description: "Frequent Poster", user_id: 212 }, + { extras: null, description: "Frequent Poster", user_id: 6118 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { + extras: "latest", + description: "Most Recent Poster, Most Posts", + user_id: 32 + } + ] + }, + { + id: 10114, + title: "Invitation expiry workflow is wonky", + fancy_title: "Invitation expiry workflow is wonky", + slug: "invitation-expiry-workflow-is-wonky", + posts_count: 14, + reply_count: 7, + highest_post_number: 14, + image_url: null, + created_at: "2013-09-30T00:59:36.000-04:00", + last_posted_at: "2014-01-13T18:51:26.000-05:00", + bumped: true, + bumped_at: "2014-01-13T18:51:26.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 176, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 1 }, + { extras: null, description: "Most Posts", user_id: 7076 }, + { extras: null, description: "Frequent Poster", user_id: 2 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 6330, + title: "Reply not disabled if topic closed while viewing", + fancy_title: "Reply not disabled if topic closed while viewing", + slug: "reply-not-disabled-if-topic-closed-while-viewing", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: + "https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon", + created_at: "2013-05-02T06:02:06.000-04:00", + last_posted_at: "2014-01-13T11:54:22.000-05:00", + bumped: true, + bumped_at: "2014-01-13T11:54:22.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 164, + like_count: 1, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 4851 }, + { extras: null, description: "Most Posts", user_id: 2 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 8367, + title: "Very fast scrolling fails to mark all posts read in a thread", + fancy_title: + "Very fast scrolling fails to mark all posts read in a thread", + slug: "very-fast-scrolling-fails-to-mark-all-posts-read-in-a-thread", + posts_count: 11, + reply_count: 7, + highest_post_number: 13, + image_url: null, + created_at: "2013-07-14T12:37:02.000-04:00", + last_posted_at: "2014-01-13T11:16:56.000-05:00", + bumped: true, + bumped_at: "2014-01-13T11:16:33.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 288, + like_count: 5, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 4457 }, + { extras: null, description: "Most Posts", user_id: 6280 }, + { extras: null, description: "Frequent Poster", user_id: 3681 }, + { extras: null, description: "Frequent Poster", user_id: 1621 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 8815, + title: "Cache headers confuse proxies", + fancy_title: "Cache headers confuse proxies", + slug: "cache-headers-confuse-proxies", + posts_count: 9, + reply_count: 3, + highest_post_number: 9, + image_url: null, + created_at: "2013-08-02T05:45:26.000-04:00", + last_posted_at: "2014-01-13T11:12:09.000-05:00", + bumped: true, + bumped_at: "2014-01-13T10:41:44.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 314, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 6266 }, + { extras: null, description: "Most Posts", user_id: 19 }, + { extras: null, description: "Frequent Poster", user_id: 1 }, + { extras: null, description: "Frequent Poster", user_id: 4457 }, + { + extras: "latest", + description: "Most Recent Poster, Frequent Poster", + user_id: 32 + } + ] + }, + { + id: 11371, + title: "Search not working for Staff users", + fancy_title: "Search not working for Staff users", + slug: "search-not-working-for-staff-users", + posts_count: 15, + reply_count: 10, + highest_post_number: 15, + image_url: null, + created_at: "2013-12-11T13:22:56.000-05:00", + last_posted_at: "2014-01-13T01:41:50.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:41:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 217, + like_count: 4, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 5335 }, + { extras: null, description: "Most Posts", user_id: 19 }, + { extras: null, description: "Frequent Poster", user_id: 6314 }, + { extras: null, description: "Frequent Poster", user_id: 32 }, + { extras: "latest", description: "Most Recent Poster", user_id: 1 } + ] + }, + { + id: 9908, + title: "Draft bar overrides pagination widget", + fancy_title: "Draft bar overrides pagination widget", + slug: "draft-bar-overrides-pagination-widget", + posts_count: 4, + reply_count: 0, + highest_post_number: 4, + image_url: null, + created_at: "2013-09-19T17:19:52.000-04:00", + last_posted_at: "2014-01-13T01:26:01.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:25:12.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: true, + views: 108, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 5351 }, + { extras: null, description: "Most Posts", user_id: 471 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 6134, + title: + "Unread topic is stuck as unread after insertion of staff message", + fancy_title: + "Unread topic is stuck as unread after insertion of staff message", + slug: + "unread-topic-is-stuck-as-unread-after-insertion-of-staff-message", + posts_count: 5, + reply_count: 1, + highest_post_number: 5, + image_url: + "https://www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s=40&r=pg&d=identicon", + created_at: "2013-04-24T13:37:32.000-04:00", + last_posted_at: "2014-01-13T01:22:49.000-05:00", + bumped: true, + bumped_at: "2014-01-13T01:22:42.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: true, + archived: false, + views: 169, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 7, + posters: [ + { extras: null, description: "Original Poster", user_id: 3681 }, + { extras: null, description: "Most Posts", user_id: 5351 }, + { extras: "latest", description: "Most Recent Poster", user_id: 32 } + ] + }, + { + id: 11914, + title: "Google analytics is not registering page views", + fancy_title: "Google analytics is not registering page views", + slug: "google-analytics-is-not-registering-page-views", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2014-01-13T00:32:45.000-05:00", + last_posted_at: "2014-01-13T00:32:46.000-05:00", + bumped: true, + bumped_at: "2014-01-13T00:32:46.000-05:00", + unseen: false, + pinned: false, + visible: true, + closed: false, + archived: false, + views: 37, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 7, + posters: [ + { + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 1 + } + ] + } + ] + } + }, + "/categories_and_latest.json": { + category_list: { + can_create_category: false, + can_create_topic: false, + draft: null, + draft_key: "new_topic", + draft_sequence: null, + categories: [ + { + id: 1, + name: "Uncategorized", + color: "AB9364", + text_color: "FFFFFF", + slug: "uncategorized", + topic_count: 1, + post_count: 0, + position: 0, + description: + "Topics that don't need a category, or don't fit into any other existing category.", + description_text: "", + topic_url: null, + logo_url: null, + background_url: null, + read_restricted: false, + permission: null, + notification_level: null, + topic_template: null, + has_children: false, + topics_day: 0, + topics_week: 0, + topics_month: 0, + topics_year: 0, + topics_all_time: 1, + description_excerpt: + "Topics that don't need a category, or don't fit into any other existing category.", + is_uncategorized: true + }, + { + id: 3, + name: "Site Feedback", + color: "808281", + text_color: "FFFFFF", + slug: "site-feedback", + topic_count: 0, + post_count: 0, + position: 1, + description: + "Discussion about this site, its organization, how it works, and how we can improve it.", + description_text: + "Discussion about this site, its organization, how it works, and how we can improve it.", + topic_url: "/t/about-the-site-feedback-category/2", + logo_url: null, + background_url: null, + read_restricted: false, + permission: null, + notification_level: null, + topic_template: null, + has_children: false, + topics_day: 0, + topics_week: 0, + topics_month: 0, + topics_year: 0, + topics_all_time: 0, + description_excerpt: + "Discussion about this site, its organization, how it works, and how we can improve it." + } + ] + }, + topic_list: { + can_create_topic: false, + draft: null, + draft_key: "new_topic", + draft_sequence: null, + per_page: 30, + topics: [ + { + id: 8, + title: "Welcome to Discourse", + fancy_title: "Welcome to Discourse", + slug: "welcome-to-discourse", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2016-08-29T20:38:19.359Z", + last_posted_at: "2016-08-29T20:38:19.402Z", + bumped: true, + bumped_at: "2016-08-29T20:38:19.402Z", + unseen: false, + pinned: true, + unpinned: null, + excerpt: + "The first paragraph of this pinned topic will be visible as a welcome message to all new visitors on your homepage. It's important! \n\nEdit this into a brief description of your community: \n\n\nWho is it for?\nWhat can they …", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 0, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "system", + category_id: 1, + pinned_globally: true, + posters: [ + { + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: -1 + } + ] + } + ] + } + } }; diff --git a/test/javascripts/fixtures/group-fixtures.js.es6 b/test/javascripts/fixtures/group-fixtures.js.es6 index 4493e65df7..50adde6fef 100644 --- a/test/javascripts/fixtures/group-fixtures.js.es6 +++ b/test/javascripts/fixtures/group-fixtures.js.es6 @@ -1,1230 +1,1299 @@ export default { - "/groups/moderators.json":{ - "group": { - "id": 50, - "automatic": true, - "name": "moderators", - "display_name": "moderators", - "mentionable_level": 0, - "messageable_level": 99, - "visibility_level": 0, - "automatic_membership_email_domains": null, - "automatic_membership_retroactive": false, - "primary_group": false, - "title": null, - "grant_trust_level": null, - "incoming_email": null, - "has_messages": true, - "flair_url": null, - "flair_bg_color": null, - "flair_color": null, - "bio_raw": null, - "bio_cooked": null, - "public_admission": false, - "public_exit": false, - "allow_membership_requests": false, - "full_name": null, - "default_notification_level": 2, - "membership_request_template": null, - "is_group_user": true, - "is_group_owner": true, - "mentionable": false, - "messageable": true - }, - }, - "/groups/discourse.json":{ - "group":{ - "id":47, - "automatic":false, - "name":"discourse", - "full_name":"Awesome Team", - "user_count":8, - "alias_level":99, - "visible":true, - "public_admission":true, - "public_exit":false, - "flair_url": 'fa-adjust', - "is_group_owner":true, - "mentionable":true, - "messageable":true - }, - "extras": { - "visible_group_names": ["discourse"] + "/groups/moderators.json": { + group: { + id: 50, + automatic: true, + name: "moderators", + display_name: "moderators", + mentionable_level: 0, + messageable_level: 99, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: true, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 2, + membership_request_template: null, + is_group_user: true, + is_group_owner: true, + mentionable: false, + messageable: true } }, - "/topics/groups/discourse.json":{ - "users":[ + "/groups/discourse.json": { + group: { + id: 47, + automatic: false, + name: "discourse", + full_name: "Awesome Team", + user_count: 8, + alias_level: 99, + visible: true, + public_admission: true, + public_exit: false, + flair_url: "fa-adjust", + is_group_owner: true, + mentionable: true, + messageable: true + }, + extras: { + visible_group_names: ["discourse"] + } + }, + "/topics/groups/discourse.json": { + users: [ { - "id":2, - "username":"bruce1", - "avatar_template":"/user_avatar/meta.discourse.org/bruce1/{size}/5245.png" + id: 2, + username: "bruce1", + avatar_template: + "/user_avatar/meta.discourse.org/bruce1/{size}/5245.png" }, { - "id":1, - "username":"bruce0", - "avatar_template":"/user_avatar/meta.discourse.org/bruce0/{size}/5245.png" + id: 1, + username: "bruce0", + avatar_template: + "/user_avatar/meta.discourse.org/bruce0/{size}/5245.png" } ], - "primary_groups":[], - "topic_list":{ - "can_create_topic":true, - "draft":null, - "draft_key":"new_topic", - "draft_sequence":1, - "per_page":30, - "topics":[ + primary_groups: [], + topic_list: { + can_create_topic: true, + draft: null, + draft_key: "new_topic", + draft_sequence: 1, + per_page: 30, + topics: [ { - "id":12074, - "title":"This is a test topic 1", - "fancy_title":"This is a test topic 1", - "slug":"this-is-a-test-topic-1", - "posts_count":0, - "reply_count":0, - "highest_post_number":0, - "image_url":null, - "created_at":"2018-03-15T03:12:48.955Z", - "last_posted_at":null, - "bumped":true, - "bumped_at":"2018-03-15T03:12:48.955Z", - "unseen":true, - "pinned":false, - "unpinned":null, - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":0, - "like_count":0, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"bruce1", - "category_id":1, - "pinned_globally":false, - "featured_link":null, - "posters":[ + id: 12074, + title: "This is a test topic 1", + fancy_title: "This is a test topic 1", + slug: "this-is-a-test-topic-1", + posts_count: 0, + reply_count: 0, + highest_post_number: 0, + image_url: null, + created_at: "2018-03-15T03:12:48.955Z", + last_posted_at: null, + bumped: true, + bumped_at: "2018-03-15T03:12:48.955Z", + unseen: true, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 0, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "bruce1", + category_id: 1, + pinned_globally: false, + featured_link: null, + posters: [ { - "extras":"latest single", - "description":"Original Poster, Most Recent Poster", - "user_id":2, - "primary_group_id":null + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 2, + primary_group_id: null } ] }, { - "id":12073, - "title":"This is a test topic 0", - "fancy_title":"This is a test topic 0", - "slug":"this-is-a-test-topic-0", - "posts_count":0, - "reply_count":0, - "highest_post_number":0, - "image_url":null, - "created_at":"2018-03-15T03:12:48.899Z", - "last_posted_at":null, - "bumped":true, - "bumped_at":"2018-03-15T03:12:48.900Z", - "unseen":true, - "pinned":false, - "unpinned":null, - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":0, - "like_count":0, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"bruce0", - "category_id":1, - "pinned_globally":false, - "featured_link":null, - "posters":[ + id: 12073, + title: "This is a test topic 0", + fancy_title: "This is a test topic 0", + slug: "this-is-a-test-topic-0", + posts_count: 0, + reply_count: 0, + highest_post_number: 0, + image_url: null, + created_at: "2018-03-15T03:12:48.899Z", + last_posted_at: null, + bumped: true, + bumped_at: "2018-03-15T03:12:48.900Z", + unseen: true, + pinned: false, + unpinned: null, + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 0, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "bruce0", + category_id: 1, + pinned_globally: false, + featured_link: null, + posters: [ { - "extras":"latest single", - "description":"Original Poster, Most Recent Poster", - "user_id":1, - "primary_group_id":null + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 1, + primary_group_id: null } ] } ] } }, - "/groups/discourse/counts.json":{ - "counts":{ - "posts":17829, - "members":7 + "/groups/discourse/counts.json": { + counts: { + posts: 17829, + members: 7 } }, - "/groups/discourse/members.json":{ - "owners":[ - - ], - "members":[ + "/groups/discourse/members.json": { + owners: [], + members: [ { - "id":2770, - "username":"awesomerobot", - "uploaded_avatar_id":33872, - "avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png", - "name":"", - "last_seen_at":"2015-01-23T15:53:17.844Z" + id: 2770, + username: "awesomerobot", + uploaded_avatar_id: 33872, + avatar_template: + "/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png", + name: "", + last_seen_at: "2015-01-23T15:53:17.844Z" }, { - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png", - "name":"Jeff Atwood", - "last_seen_at":"2015-01-23T06:05:25.457Z" + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png", + name: "Jeff Atwood", + last_seen_at: "2015-01-23T06:05:25.457Z" }, { - "id":19, - "username":"eviltrout", - "uploaded_avatar_id":5275, - "avatar_template":"/user_avatar/meta.discourse.org/eviltrout/{size}/5275.png", - "name":"Robin Ward", - "last_seen_at":"2015-01-23T16:03:45.098Z" + id: 19, + username: "eviltrout", + uploaded_avatar_id: 5275, + avatar_template: + "/user_avatar/meta.discourse.org/eviltrout/{size}/5275.png", + name: "Robin Ward", + last_seen_at: "2015-01-23T16:03:45.098Z" }, { - "id":2, - "username":"neil", - "uploaded_avatar_id":5245, - "avatar_template":"/user_avatar/meta.discourse.org/neil/{size}/5245.png", - "name":"Neil Lalonde", - "last_seen_at":"2015-01-23T15:22:10.244Z" + id: 2, + username: "neil", + uploaded_avatar_id: 5245, + avatar_template: "/user_avatar/meta.discourse.org/neil/{size}/5245.png", + name: "Neil Lalonde", + last_seen_at: "2015-01-23T15:22:10.244Z" }, { - "id":1, - "username":"sam", - "uploaded_avatar_id":5243, - "avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png", - "name":"Sam Saffron", - "last_seen_at":"2015-01-23T11:07:06.233Z" + id: 1, + username: "sam", + uploaded_avatar_id: 5243, + avatar_template: "/user_avatar/meta.discourse.org/sam/{size}/5243.png", + name: "Sam Saffron", + last_seen_at: "2015-01-23T11:07:06.233Z" }, { - "id":3, - "username":"supermathie", - "uploaded_avatar_id":34097, - "avatar_template":"/user_avatar/meta.discourse.org/supermathie/{size}/34097.png", - "name":"Michael Brown", - "last_seen_at":"2015-01-22T05:16:42.254Z" + id: 3, + username: "supermathie", + uploaded_avatar_id: 34097, + avatar_template: + "/user_avatar/meta.discourse.org/supermathie/{size}/34097.png", + name: "Michael Brown", + last_seen_at: "2015-01-22T05:16:42.254Z" }, { - "id":1995, - "username":"zogstrip", - "uploaded_avatar_id":8630, - "avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png", - "name":"Régis Hanol", - "last_seen_at":"2015-01-23T15:45:34.196Z" + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: + "/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png", + name: "Régis Hanol", + last_seen_at: "2015-01-23T15:45:34.196Z" } ], - "meta":{ - "total":7, - "limit":50, - "offset":0 + meta: { + total: 7, + limit: 50, + offset: 0 } }, - "/groups/discourse/posts.json":[ + "/groups/discourse/posts.json": [ { - "id":94607, - "cooked":"

I don't know how to pronounce that in English, but this makes me think of the French word \"disquette\" (floppy disk)

", - "created_at":"2015-01-23T15:13:01.935Z", - "title":"Consistent new indicator", - "url":"/t/consistent-new-indicator/24355/1", - "user_title":"designerator", - "user_long_name":"", - "category":{ - "id":9, - "name":"ux", - "color":"5F497A", - "topic_id":2628, - "topic_count":540, - "created_at":"2013-02-10T03:52:21.322Z", - "updated_at":"2015-01-22T18:05:32.152Z", - "user_id":32, - "topics_year":370, - "topics_month":33, - "topics_week":3, - "slug":"ux", - "description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":5823, - "latest_post_id":94610, - "latest_topic_id":24355, - "position":25, - "parent_category_id":null, - "posts_year":4264, - "posts_month":609, - "posts_week":103, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":28, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"ux", - "auto_close_based_on_last_post":false + id: 94607, + cooked: + '

I don\'t know how to pronounce that in English, but this makes me think of the French word "disquette" (floppy disk)

', + created_at: "2015-01-23T15:13:01.935Z", + title: "Consistent new indicator", + url: "/t/consistent-new-indicator/24355/1", + user_title: "designerator", + user_long_name: "", + category: { + id: 9, + name: "ux", + color: "5F497A", + topic_id: 2628, + topic_count: 540, + created_at: "2013-02-10T03:52:21.322Z", + updated_at: "2015-01-22T18:05:32.152Z", + user_id: 32, + topics_year: 370, + topics_month: 33, + topics_week: 3, + slug: "ux", + description: + "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 5823, + latest_post_id: 94610, + latest_topic_id: 24355, + position: 25, + parent_category_id: null, + posts_year: 4264, + posts_month: 609, + posts_week: 103, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 28, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "ux", + auto_close_based_on_last_post: false }, - "user":{ - "id":2770, - "username":"awesomerobot", - "uploaded_avatar_id":33872, - "avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png" + user: { + id: 2770, + username: "awesomerobot", + uploaded_avatar_id: 33872, + avatar_template: + "/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png" } }, { - "id":94603, - "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", - "created_at":"2015-01-23T14:59:21.941Z", - "title":"The end of Clown Vomit, or, simplified category styles", - "url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/63", - "user_title":"designerator", - "user_long_name":"", - "category":{ - "id":9, - "name":"ux", - "color":"5F497A", - "topic_id":2628, - "topic_count":540, - "created_at":"2013-02-10T03:52:21.322Z", - "updated_at":"2015-01-22T18:05:32.152Z", - "user_id":32, - "topics_year":370, - "topics_month":33, - "topics_week":3, - "slug":"ux", - "description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":5823, - "latest_post_id":94610, - "latest_topic_id":24355, - "position":25, - "parent_category_id":null, - "posts_year":4264, - "posts_month":609, - "posts_week":103, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":28, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"ux", - "auto_close_based_on_last_post":false + id: 94603, + cooked: + "

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", + created_at: "2015-01-23T14:59:21.941Z", + title: "The end of Clown Vomit, or, simplified category styles", + url: "/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/63", + user_title: "designerator", + user_long_name: "", + category: { + id: 9, + name: "ux", + color: "5F497A", + topic_id: 2628, + topic_count: 540, + created_at: "2013-02-10T03:52:21.322Z", + updated_at: "2015-01-22T18:05:32.152Z", + user_id: 32, + topics_year: 370, + topics_month: 33, + topics_week: 3, + slug: "ux", + description: + "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 5823, + latest_post_id: 94610, + latest_topic_id: 24355, + position: 25, + parent_category_id: null, + posts_year: 4264, + posts_month: 609, + posts_week: 103, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 28, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "ux", + auto_close_based_on_last_post: false }, - "user":{ - "id":2770, - "username":"awesomerobot", - "uploaded_avatar_id":33872, - "avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png" + user: { + id: 2770, + username: "awesomerobot", + uploaded_avatar_id: 33872, + avatar_template: + "/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png" } }, { - "id":94601, - "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", - "created_at":"2015-01-23T14:51:55.497Z", - "title":"The end of Clown Vomit, or, simplified category styles", - "url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/62", - "user_title":"designerator", - "user_long_name":"", - "category":{ - "id":9, - "name":"ux", - "color":"5F497A", - "topic_id":2628, - "topic_count":540, - "created_at":"2013-02-10T03:52:21.322Z", - "updated_at":"2015-01-22T18:05:32.152Z", - "user_id":32, - "topics_year":370, - "topics_month":33, - "topics_week":3, - "slug":"ux", - "description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":5823, - "latest_post_id":94610, - "latest_topic_id":24355, - "position":25, - "parent_category_id":null, - "posts_year":4264, - "posts_month":609, - "posts_week":103, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":28, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"ux", - "auto_close_based_on_last_post":false + id: 94601, + cooked: + "

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", + created_at: "2015-01-23T14:51:55.497Z", + title: "The end of Clown Vomit, or, simplified category styles", + url: "/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/62", + user_title: "designerator", + user_long_name: "", + category: { + id: 9, + name: "ux", + color: "5F497A", + topic_id: 2628, + topic_count: 540, + created_at: "2013-02-10T03:52:21.322Z", + updated_at: "2015-01-22T18:05:32.152Z", + user_id: 32, + topics_year: 370, + topics_month: 33, + topics_week: 3, + slug: "ux", + description: + "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 5823, + latest_post_id: 94610, + latest_topic_id: 24355, + position: 25, + parent_category_id: null, + posts_year: 4264, + posts_month: 609, + posts_week: 103, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 28, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "ux", + auto_close_based_on_last_post: false }, - "user":{ - "id":2770, - "username":"awesomerobot", - "uploaded_avatar_id":33872, - "avatar_template":"/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png" + user: { + id: 2770, + username: "awesomerobot", + uploaded_avatar_id: 33872, + avatar_template: + "/user_avatar/meta.discourse.org/awesomerobot/{size}/33872.png" } }, { - "id":94577, - "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", - "created_at":"2015-01-23T10:50:55.846Z", - "title":"Quote reply insertion at cursor position", - "url":"/t/quote-reply-insertion-at-cursor-position/24344/4", - "user_title":"team", - "user_long_name":"Régis Hanol", - "category":{ - "id":2, - "name":"feature", - "color":"0E76BD", - "topic_id":11, - "topic_count":1592, - "created_at":"2013-02-02T21:42:52.552Z", - "updated_at":"2015-01-22T18:05:32.647Z", - "user_id":1, - "topics_year":919, - "topics_month":60, - "topics_week":20, - "slug":"feature", - "description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":14360, - "latest_post_id":94600, - "latest_topic_id":24344, - "position":25, - "parent_category_id":null, - "posts_year":8617, - "posts_month":690, - "posts_week":190, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":2, - "posts_day":8, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"feature", - "auto_close_based_on_last_post":false + id: 94577, + cooked: + "

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", + created_at: "2015-01-23T10:50:55.846Z", + title: "Quote reply insertion at cursor position", + url: "/t/quote-reply-insertion-at-cursor-position/24344/4", + user_title: "team", + user_long_name: "Régis Hanol", + category: { + id: 2, + name: "feature", + color: "0E76BD", + topic_id: 11, + topic_count: 1592, + created_at: "2013-02-02T21:42:52.552Z", + updated_at: "2015-01-22T18:05:32.647Z", + user_id: 1, + topics_year: 919, + topics_month: 60, + topics_week: 20, + slug: "feature", + description: + "Discussion about features or potential features of Discourse: how they work, why they work, etc.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 14360, + latest_post_id: 94600, + latest_topic_id: 24344, + position: 25, + parent_category_id: null, + posts_year: 8617, + posts_month: 690, + posts_week: 190, + email_in: null, + email_in_allow_strangers: false, + topics_day: 2, + posts_day: 8, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "feature", + auto_close_based_on_last_post: false }, - "user":{ - "id":1995, - "username":"zogstrip", - "uploaded_avatar_id":8630, - "avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" + user: { + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: + "/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" } }, { - "id":94574, - "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", - "created_at":"2015-01-23T10:31:29.222Z", - "title":"Quote reply insertion at cursor position", - "url":"/t/quote-reply-insertion-at-cursor-position/24344/2", - "user_title":"team", - "user_long_name":"Régis Hanol", - "category":{ - "id":2, - "name":"feature", - "color":"0E76BD", - "topic_id":11, - "topic_count":1592, - "created_at":"2013-02-02T21:42:52.552Z", - "updated_at":"2015-01-22T18:05:32.647Z", - "user_id":1, - "topics_year":919, - "topics_month":60, - "topics_week":20, - "slug":"feature", - "description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":14360, - "latest_post_id":94600, - "latest_topic_id":24344, - "position":25, - "parent_category_id":null, - "posts_year":8617, - "posts_month":690, - "posts_week":190, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":2, - "posts_day":8, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"feature", - "auto_close_based_on_last_post":false + id: 94574, + cooked: + "

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", + created_at: "2015-01-23T10:31:29.222Z", + title: "Quote reply insertion at cursor position", + url: "/t/quote-reply-insertion-at-cursor-position/24344/2", + user_title: "team", + user_long_name: "Régis Hanol", + category: { + id: 2, + name: "feature", + color: "0E76BD", + topic_id: 11, + topic_count: 1592, + created_at: "2013-02-02T21:42:52.552Z", + updated_at: "2015-01-22T18:05:32.647Z", + user_id: 1, + topics_year: 919, + topics_month: 60, + topics_week: 20, + slug: "feature", + description: + "Discussion about features or potential features of Discourse: how they work, why they work, etc.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 14360, + latest_post_id: 94600, + latest_topic_id: 24344, + position: 25, + parent_category_id: null, + posts_year: 8617, + posts_month: 690, + posts_week: 190, + email_in: null, + email_in_allow_strangers: false, + topics_day: 2, + posts_day: 8, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "feature", + auto_close_based_on_last_post: false }, - "user":{ - "id":1995, - "username":"zogstrip", - "uploaded_avatar_id":8630, - "avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" + user: { + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: + "/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" } }, { - "id":94572, - "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", - "created_at":"2015-01-23T09:46:00.901Z", - "title":"Translations frequently broken", - "url":"/t/translations-frequently-broken/22546/27", - "user_title":"team", - "user_long_name":"Régis Hanol", - "category":{ - "id":27, - "name":"translations", - "color":"808281", - "topic_id":14549, - "topic_count":146, - "created_at":"2014-04-07T20:30:17.623Z", - "updated_at":"2015-01-22T18:05:33.111Z", - "user_id":2, - "topics_year":134, - "topics_month":5, - "topics_week":3, - "slug":"translations", - "description":"This category is for discussion about localizing Discourse.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":1167, - "latest_post_id":94575, - "latest_topic_id":24301, - "position":25, - "parent_category_id":7, - "posts_year":965, - "posts_month":60, - "posts_week":29, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":1, - "posts_day":5, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"translations", - "auto_close_based_on_last_post":false + id: 94572, + cooked: + "

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", + created_at: "2015-01-23T09:46:00.901Z", + title: "Translations frequently broken", + url: "/t/translations-frequently-broken/22546/27", + user_title: "team", + user_long_name: "Régis Hanol", + category: { + id: 27, + name: "translations", + color: "808281", + topic_id: 14549, + topic_count: 146, + created_at: "2014-04-07T20:30:17.623Z", + updated_at: "2015-01-22T18:05:33.111Z", + user_id: 2, + topics_year: 134, + topics_month: 5, + topics_week: 3, + slug: "translations", + description: + "This category is for discussion about localizing Discourse.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 1167, + latest_post_id: 94575, + latest_topic_id: 24301, + position: 25, + parent_category_id: 7, + posts_year: 965, + posts_month: 60, + posts_week: 29, + email_in: null, + email_in_allow_strangers: false, + topics_day: 1, + posts_day: 5, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "translations", + auto_close_based_on_last_post: false }, - "user":{ - "id":1995, - "username":"zogstrip", - "uploaded_avatar_id":8630, - "avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" + user: { + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: + "/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" } }, { - "id":94555, - "cooked":"

I don't know how to pronounce that in English, but this makes me think of the French word \"disquette\" (floppy disk)

", - "created_at":"2015-01-23T08:17:31.700Z", - "title":"Introducing Discette - a minimal ember-cli front end to Discourse", - "url":"/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/3", - "user_title":"team", - "user_long_name":"Régis Hanol", - "category":{ - "id":7, - "name":"dev", - "color":"000", - "topic_id":1026, - "topic_count":574, - "created_at":"2013-02-06T08:43:41.550Z", - "updated_at":"2015-01-22T18:05:32.855Z", - "user_id":32, - "topics_year":298, - "topics_month":29, - "topics_week":2, - "slug":"dev", - "description":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":4196, - "latest_post_id":94590, - "latest_topic_id":24349, - "position":25, - "parent_category_id":null, - "posts_year":2095, - "posts_month":172, - "posts_week":16, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":3, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"dev", - "auto_close_based_on_last_post":false + id: 94555, + cooked: + '

I don\'t know how to pronounce that in English, but this makes me think of the French word "disquette" (floppy disk)

', + created_at: "2015-01-23T08:17:31.700Z", + title: + "Introducing Discette - a minimal ember-cli front end to Discourse", + url: + "/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/3", + user_title: "team", + user_long_name: "Régis Hanol", + category: { + id: 7, + name: "dev", + color: "000", + topic_id: 1026, + topic_count: 574, + created_at: "2013-02-06T08:43:41.550Z", + updated_at: "2015-01-22T18:05:32.855Z", + user_id: 32, + topics_year: 298, + topics_month: 29, + topics_week: 2, + slug: "dev", + description: + "This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 4196, + latest_post_id: 94590, + latest_topic_id: 24349, + position: 25, + parent_category_id: null, + posts_year: 2095, + posts_month: 172, + posts_week: 16, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 3, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "dev", + auto_close_based_on_last_post: false }, - "user":{ - "id":1995, - "username":"zogstrip", - "uploaded_avatar_id":8630, - "avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" + user: { + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: + "/user_avatar/meta.discourse.org/zogstrip/{size}/8630.png" } }, { - "id":94544, - "cooked":"

@techapj fixed this for 1.2.

", - "created_at":"2015-01-23T05:49:35.881Z", - "title":"After sign-in, I'm not redirected to the conversation", - "url":"/t/after-sign-in-im-not-redirected-to-the-conversation/17753/8", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":9, - "name":"ux", - "color":"5F497A", - "topic_id":2628, - "topic_count":540, - "created_at":"2013-02-10T03:52:21.322Z", - "updated_at":"2015-01-22T18:05:32.152Z", - "user_id":32, - "topics_year":370, - "topics_month":33, - "topics_week":3, - "slug":"ux", - "description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":5823, - "latest_post_id":94610, - "latest_topic_id":24355, - "position":25, - "parent_category_id":null, - "posts_year":4264, - "posts_month":609, - "posts_week":103, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":28, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"ux", - "auto_close_based_on_last_post":false + id: 94544, + cooked: + '

@techapj fixed this for 1.2.

', + created_at: "2015-01-23T05:49:35.881Z", + title: "After sign-in, I'm not redirected to the conversation", + url: "/t/after-sign-in-im-not-redirected-to-the-conversation/17753/8", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 9, + name: "ux", + color: "5F497A", + topic_id: 2628, + topic_count: 540, + created_at: "2013-02-10T03:52:21.322Z", + updated_at: "2015-01-22T18:05:32.152Z", + user_id: 32, + topics_year: 370, + topics_month: 33, + topics_week: 3, + slug: "ux", + description: + "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 5823, + latest_post_id: 94610, + latest_topic_id: 24355, + position: 25, + parent_category_id: null, + posts_year: 4264, + posts_month: 609, + posts_week: 103, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 28, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "ux", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94543, - "cooked":"

Oh yes IOS 8.2 -- well, let's see what happens because there is really no fix on our end. Basic HTML / CSS stuff is broken.

", - "created_at":"2015-01-23T05:45:40.306Z", - "title":"Dealing with iOS 8 Mobile Safari bugs?", - "url":"/t/dealing-with-ios-8-mobile-safari-bugs/24101/7", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":2, - "name":"feature", - "color":"0E76BD", - "topic_id":11, - "topic_count":1592, - "created_at":"2013-02-02T21:42:52.552Z", - "updated_at":"2015-01-22T18:05:32.647Z", - "user_id":1, - "topics_year":919, - "topics_month":60, - "topics_week":20, - "slug":"feature", - "description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":14360, - "latest_post_id":94600, - "latest_topic_id":24344, - "position":25, - "parent_category_id":null, - "posts_year":8617, - "posts_month":690, - "posts_week":190, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":2, - "posts_day":8, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"feature", - "auto_close_based_on_last_post":false + id: 94543, + cooked: + "

Oh yes IOS 8.2 -- well, let's see what happens because there is really no fix on our end. Basic HTML / CSS stuff is broken.

", + created_at: "2015-01-23T05:45:40.306Z", + title: "Dealing with iOS 8 Mobile Safari bugs?", + url: "/t/dealing-with-ios-8-mobile-safari-bugs/24101/7", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 2, + name: "feature", + color: "0E76BD", + topic_id: 11, + topic_count: 1592, + created_at: "2013-02-02T21:42:52.552Z", + updated_at: "2015-01-22T18:05:32.647Z", + user_id: 1, + topics_year: 919, + topics_month: 60, + topics_week: 20, + slug: "feature", + description: + "Discussion about features or potential features of Discourse: how they work, why they work, etc.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 14360, + latest_post_id: 94600, + latest_topic_id: 24344, + position: 25, + parent_category_id: null, + posts_year: 8617, + posts_month: 690, + posts_week: 190, + email_in: null, + email_in_allow_strangers: false, + topics_day: 2, + posts_day: 8, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "feature", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94542, - "cooked":"

Hmm that looks like a bug, @techapj can you have a look?

", - "created_at":"2015-01-23T05:43:55.602Z", - "title":"RSS is not valid", - "url":"/t/rss-is-not-valid/24338/2", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":6, - "name":"support", - "color":"CEA9A9", - "topic_id":389, - "topic_count":1781, - "created_at":"2013-02-05T22:16:38.672Z", - "updated_at":"2015-01-22T18:05:33.572Z", - "user_id":1, - "topics_year":1541, - "topics_month":167, - "topics_week":49, - "slug":"support", - "description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":12272, - "latest_post_id":94602, - "latest_topic_id":24346, - "position":25, - "parent_category_id":null, - "posts_year":10571, - "posts_month":1254, - "posts_week":413, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":5, - "posts_day":70, - "logo_url":"", - "background_url":"", - "allow_badges":true, - "name_lower":"support", - "auto_close_based_on_last_post":false + id: 94542, + cooked: + '

Hmm that looks like a bug, @techapj can you have a look?

', + created_at: "2015-01-23T05:43:55.602Z", + title: "RSS is not valid", + url: "/t/rss-is-not-valid/24338/2", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 6, + name: "support", + color: "CEA9A9", + topic_id: 389, + topic_count: 1781, + created_at: "2013-02-05T22:16:38.672Z", + updated_at: "2015-01-22T18:05:33.572Z", + user_id: 1, + topics_year: 1541, + topics_month: 167, + topics_week: 49, + slug: "support", + description: + "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 12272, + latest_post_id: 94602, + latest_topic_id: 24346, + position: 25, + parent_category_id: null, + posts_year: 10571, + posts_month: 1254, + posts_week: 413, + email_in: null, + email_in_allow_strangers: false, + topics_day: 5, + posts_day: 70, + logo_url: "", + background_url: "", + allow_badges: true, + name_lower: "support", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94522, - "cooked":"

Oh I see. @zogstrip can you have a look?

", - "created_at":"2015-01-23T03:00:20.485Z", - "title":"Pasted image upload size error", - "url":"/t/pasted-image-upload-size-error/24320/4", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":1, - "name":"bug", - "color":"e9dd00", - "topic_id":2, - "topic_count":1729, - "created_at":"2013-02-01T04:56:34.914Z", - "updated_at":"2015-01-22T18:05:33.426Z", - "user_id":1, - "topics_year":1114, - "topics_month":69, - "topics_week":22, - "slug":"bug", - "description":"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", - "text_color":"000000", - "read_restricted":false, - "auto_close_hours":null, - "post_count":11179, - "latest_post_id":94611, - "latest_topic_id":24350, - "position":25, - "parent_category_id":null, - "posts_year":7138, - "posts_month":397, - "posts_week":121, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":1, - "posts_day":6, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"bug", - "auto_close_based_on_last_post":false + id: 94522, + cooked: + '

Oh I see. @zogstrip can you have a look?

', + created_at: "2015-01-23T03:00:20.485Z", + title: "Pasted image upload size error", + url: "/t/pasted-image-upload-size-error/24320/4", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 1, + name: "bug", + color: "e9dd00", + topic_id: 2, + topic_count: 1729, + created_at: "2013-02-01T04:56:34.914Z", + updated_at: "2015-01-22T18:05:33.426Z", + user_id: 1, + topics_year: 1114, + topics_month: 69, + topics_week: 22, + slug: "bug", + description: + "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", + text_color: "000000", + read_restricted: false, + auto_close_hours: null, + post_count: 11179, + latest_post_id: 94611, + latest_topic_id: 24350, + position: 25, + parent_category_id: null, + posts_year: 7138, + posts_month: 397, + posts_week: 121, + email_in: null, + email_in_allow_strangers: false, + topics_day: 1, + posts_day: 6, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "bug", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94521, - "cooked":"

@techapj fixed this for 1.2.

", - "created_at":"2015-01-23T02:58:27.451Z", - "title":"The end of Clown Vomit, or, simplified category styles", - "url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/57", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":9, - "name":"ux", - "color":"5F497A", - "topic_id":2628, - "topic_count":540, - "created_at":"2013-02-10T03:52:21.322Z", - "updated_at":"2015-01-22T18:05:32.152Z", - "user_id":32, - "topics_year":370, - "topics_month":33, - "topics_week":3, - "slug":"ux", - "description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":5823, - "latest_post_id":94610, - "latest_topic_id":24355, - "position":25, - "parent_category_id":null, - "posts_year":4264, - "posts_month":609, - "posts_week":103, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":28, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"ux", - "auto_close_based_on_last_post":false + id: 94521, + cooked: + '

@techapj fixed this for 1.2.

', + created_at: "2015-01-23T02:58:27.451Z", + title: "The end of Clown Vomit, or, simplified category styles", + url: "/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/57", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 9, + name: "ux", + color: "5F497A", + topic_id: 2628, + topic_count: 540, + created_at: "2013-02-10T03:52:21.322Z", + updated_at: "2015-01-22T18:05:32.152Z", + user_id: 32, + topics_year: 370, + topics_month: 33, + topics_week: 3, + slug: "ux", + description: + "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 5823, + latest_post_id: 94610, + latest_topic_id: 24355, + position: 25, + parent_category_id: null, + posts_year: 4264, + posts_month: 609, + posts_week: 103, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 28, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "ux", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94519, - "cooked":"

What would you suggest writing here that would be more clear?

", - "created_at":"2015-01-23T02:45:36.859Z", - "title":"What is \"Born mobile, born to touch\" supposed to tell me?", - "url":"/t/what-is-born-mobile-born-to-touch-supposed-to-tell-me/24329/3", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":3, - "name":"meta", - "color":"aaa", - "topic_id":24, - "topic_count":139, - "created_at":"2013-02-03T00:00:15.230Z", - "updated_at":"2015-01-22T18:05:32.797Z", - "user_id":1, - "topics_year":68, - "topics_month":5, - "topics_week":1, - "slug":"meta", - "description":"Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":1116, - "latest_post_id":94559, - "latest_topic_id":24208, - "position":25, - "parent_category_id":null, - "posts_year":553, - "posts_month":33, - "posts_week":8, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":0, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"meta", - "auto_close_based_on_last_post":false + id: 94519, + cooked: + "

What would you suggest writing here that would be more clear?

", + created_at: "2015-01-23T02:45:36.859Z", + title: 'What is "Born mobile, born to touch" supposed to tell me?', + url: "/t/what-is-born-mobile-born-to-touch-supposed-to-tell-me/24329/3", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 3, + name: "meta", + color: "aaa", + topic_id: 24, + topic_count: 139, + created_at: "2013-02-03T00:00:15.230Z", + updated_at: "2015-01-22T18:05:32.797Z", + user_id: 1, + topics_year: 68, + topics_month: 5, + topics_week: 1, + slug: "meta", + description: + "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 1116, + latest_post_id: 94559, + latest_topic_id: 24208, + position: 25, + parent_category_id: null, + posts_year: 553, + posts_month: 33, + posts_week: 8, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 0, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "meta", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94518, - "cooked":"

You should generally create topics to host things like this, then make them wiki, close them, etc.

", - "created_at":"2015-01-23T02:42:20.053Z", - "title":"How to Create Static Pages in Discourse?", - "url":"/t/how-to-create-static-pages-in-discourse/24313/2", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":6, - "name":"support", - "color":"CEA9A9", - "topic_id":389, - "topic_count":1781, - "created_at":"2013-02-05T22:16:38.672Z", - "updated_at":"2015-01-22T18:05:33.572Z", - "user_id":1, - "topics_year":1541, - "topics_month":167, - "topics_week":49, - "slug":"support", - "description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":12272, - "latest_post_id":94602, - "latest_topic_id":24346, - "position":25, - "parent_category_id":null, - "posts_year":10571, - "posts_month":1254, - "posts_week":413, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":5, - "posts_day":70, - "logo_url":"", - "background_url":"", - "allow_badges":true, - "name_lower":"support", - "auto_close_based_on_last_post":false + id: 94518, + cooked: + "

You should generally create topics to host things like this, then make them wiki, close them, etc.

", + created_at: "2015-01-23T02:42:20.053Z", + title: "How to Create Static Pages in Discourse?", + url: "/t/how-to-create-static-pages-in-discourse/24313/2", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 6, + name: "support", + color: "CEA9A9", + topic_id: 389, + topic_count: 1781, + created_at: "2013-02-05T22:16:38.672Z", + updated_at: "2015-01-22T18:05:33.572Z", + user_id: 1, + topics_year: 1541, + topics_month: 167, + topics_week: 49, + slug: "support", + description: + "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 12272, + latest_post_id: 94602, + latest_topic_id: 24346, + position: 25, + parent_category_id: null, + posts_year: 10571, + posts_month: 1254, + posts_week: 413, + email_in: null, + email_in_allow_strangers: false, + topics_day: 5, + posts_day: 70, + logo_url: "", + background_url: "", + allow_badges: true, + name_lower: "support", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94517, - "cooked":"

Doubtful this is a bug, probably dependent on the PNG encoding.

\n\n

Try using PNGOUT, or converting to 8 bit PNGOUT, to see some of the differences. And PNGOUT is lossless!

", - "created_at":"2015-01-23T02:41:30.287Z", - "title":"Pasted image upload size error", - "url":"/t/pasted-image-upload-size-error/24320/2", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":1, - "name":"bug", - "color":"e9dd00", - "topic_id":2, - "topic_count":1729, - "created_at":"2013-02-01T04:56:34.914Z", - "updated_at":"2015-01-22T18:05:33.426Z", - "user_id":1, - "topics_year":1114, - "topics_month":69, - "topics_week":22, - "slug":"bug", - "description":"A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", - "text_color":"000000", - "read_restricted":false, - "auto_close_hours":null, - "post_count":11179, - "latest_post_id":94611, - "latest_topic_id":24350, - "position":25, - "parent_category_id":null, - "posts_year":7138, - "posts_month":397, - "posts_week":121, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":1, - "posts_day":6, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"bug", - "auto_close_based_on_last_post":false + id: 94517, + cooked: + "

Doubtful this is a bug, probably dependent on the PNG encoding.

\n\n

Try using PNGOUT, or converting to 8 bit PNGOUT, to see some of the differences. And PNGOUT is lossless!

", + created_at: "2015-01-23T02:41:30.287Z", + title: "Pasted image upload size error", + url: "/t/pasted-image-upload-size-error/24320/2", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 1, + name: "bug", + color: "e9dd00", + topic_id: 2, + topic_count: 1729, + created_at: "2013-02-01T04:56:34.914Z", + updated_at: "2015-01-22T18:05:33.426Z", + user_id: 1, + topics_year: 1114, + topics_month: 69, + topics_week: 22, + slug: "bug", + description: + "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.", + text_color: "000000", + read_restricted: false, + auto_close_hours: null, + post_count: 11179, + latest_post_id: 94611, + latest_topic_id: 24350, + position: 25, + parent_category_id: null, + posts_year: 7138, + posts_month: 397, + posts_week: 121, + email_in: null, + email_in_allow_strangers: false, + topics_day: 1, + posts_day: 6, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "bug", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94516, - "cooked":"

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", - "created_at":"2015-01-23T02:40:11.726Z", - "title":"Monetizing Discourse Talk", - "url":"/t/monetizing-discourse-talk/24316/4", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":6, - "name":"support", - "color":"CEA9A9", - "topic_id":389, - "topic_count":1781, - "created_at":"2013-02-05T22:16:38.672Z", - "updated_at":"2015-01-22T18:05:33.572Z", - "user_id":1, - "topics_year":1541, - "topics_month":167, - "topics_week":49, - "slug":"support", - "description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":12272, - "latest_post_id":94602, - "latest_topic_id":24346, - "position":25, - "parent_category_id":null, - "posts_year":10571, - "posts_month":1254, - "posts_week":413, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":5, - "posts_day":70, - "logo_url":"", - "background_url":"", - "allow_badges":true, - "name_lower":"support", - "auto_close_based_on_last_post":false + id: 94516, + cooked: + "

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", + created_at: "2015-01-23T02:40:11.726Z", + title: "Monetizing Discourse Talk", + url: "/t/monetizing-discourse-talk/24316/4", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 6, + name: "support", + color: "CEA9A9", + topic_id: 389, + topic_count: 1781, + created_at: "2013-02-05T22:16:38.672Z", + updated_at: "2015-01-22T18:05:33.572Z", + user_id: 1, + topics_year: 1541, + topics_month: 167, + topics_week: 49, + slug: "support", + description: + "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 12272, + latest_post_id: 94602, + latest_topic_id: 24346, + position: 25, + parent_category_id: null, + posts_year: 10571, + posts_month: 1254, + posts_week: 413, + email_in: null, + email_in_allow_strangers: false, + topics_day: 5, + posts_day: 70, + logo_url: "", + background_url: "", + allow_badges: true, + name_lower: "support", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94515, - "cooked":"

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", - "created_at":"2015-01-23T02:38:29.185Z", - "title":"Introducing Discette - a minimal ember-cli front end to Discourse", - "url":"/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/2", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":7, - "name":"dev", - "color":"000", - "topic_id":1026, - "topic_count":574, - "created_at":"2013-02-06T08:43:41.550Z", - "updated_at":"2015-01-22T18:05:32.855Z", - "user_id":32, - "topics_year":298, - "topics_month":29, - "topics_week":2, - "slug":"dev", - "description":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":4196, - "latest_post_id":94590, - "latest_topic_id":24349, - "position":25, - "parent_category_id":null, - "posts_year":2095, - "posts_month":172, - "posts_week":16, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":0, - "posts_day":3, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"dev", - "auto_close_based_on_last_post":false + id: 94515, + cooked: + "

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", + created_at: "2015-01-23T02:38:29.185Z", + title: + "Introducing Discette - a minimal ember-cli front end to Discourse", + url: + "/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/2", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 7, + name: "dev", + color: "000", + topic_id: 1026, + topic_count: 574, + created_at: "2013-02-06T08:43:41.550Z", + updated_at: "2015-01-22T18:05:32.855Z", + user_id: 32, + topics_year: 298, + topics_month: 29, + topics_week: 2, + slug: "dev", + description: + "This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 4196, + latest_post_id: 94590, + latest_topic_id: 24349, + position: 25, + parent_category_id: null, + posts_year: 2095, + posts_month: 172, + posts_week: 16, + email_in: null, + email_in_allow_strangers: false, + topics_day: 0, + posts_day: 3, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "dev", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94514, - "cooked":"

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", - "created_at":"2015-01-23T02:37:39.518Z", - "title":"How to do \"Object Oriented Discussion\" through Oneboxes?", - "url":"/t/how-to-do-object-oriented-discussion-through-oneboxes/24328/2", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":5, - "name":"extensibility", - "color":"FE8432", - "topic_id":28, - "topic_count":295, - "created_at":"2013-02-03T08:42:06.329Z", - "updated_at":"2015-01-22T18:05:32.698Z", - "user_id":1, - "topics_year":187, - "topics_month":17, - "topics_week":7, - "slug":"extensibility", - "description":"Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":2574, - "latest_post_id":94582, - "latest_topic_id":24328, - "position":25, - "parent_category_id":null, - "posts_year":1485, - "posts_month":196, - "posts_week":52, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":2, - "posts_day":8, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"extensibility", - "auto_close_based_on_last_post":false + id: 94514, + cooked: + "

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", + created_at: "2015-01-23T02:37:39.518Z", + title: 'How to do "Object Oriented Discussion" through Oneboxes?', + url: "/t/how-to-do-object-oriented-discussion-through-oneboxes/24328/2", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 5, + name: "extensibility", + color: "FE8432", + topic_id: 28, + topic_count: 295, + created_at: "2013-02-03T08:42:06.329Z", + updated_at: "2015-01-22T18:05:32.698Z", + user_id: 1, + topics_year: 187, + topics_month: 17, + topics_week: 7, + slug: "extensibility", + description: + "Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 2574, + latest_post_id: 94582, + latest_topic_id: 24328, + position: 25, + parent_category_id: null, + posts_year: 1485, + posts_month: 196, + posts_week: 52, + email_in: null, + email_in_allow_strangers: false, + topics_day: 2, + posts_day: 8, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "extensibility", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94512, - "cooked":"

Hmm, have not seen problems updating on 1gb instance provided swap is there.

\n\n

Anything else running on the machine?

\n\n

Maybe reboot, then upgrade Docker from command line, then upgrade Discourse from command line.

", - "created_at":"2015-01-23T02:32:31.383Z", - "title":"Update Failed and Now Showing Currently Upgrading", - "url":"/t/update-failed-and-now-showing-currently-upgrading/24332/2", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":6, - "name":"support", - "color":"CEA9A9", - "topic_id":389, - "topic_count":1781, - "created_at":"2013-02-05T22:16:38.672Z", - "updated_at":"2015-01-22T18:05:33.572Z", - "user_id":1, - "topics_year":1541, - "topics_month":167, - "topics_week":49, - "slug":"support", - "description":"Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":12272, - "latest_post_id":94602, - "latest_topic_id":24346, - "position":25, - "parent_category_id":null, - "posts_year":10571, - "posts_month":1254, - "posts_week":413, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":5, - "posts_day":70, - "logo_url":"", - "background_url":"", - "allow_badges":true, - "name_lower":"support", - "auto_close_based_on_last_post":false + id: 94512, + cooked: + "

Hmm, have not seen problems updating on 1gb instance provided swap is there.

\n\n

Anything else running on the machine?

\n\n

Maybe reboot, then upgrade Docker from command line, then upgrade Discourse from command line.

", + created_at: "2015-01-23T02:32:31.383Z", + title: "Update Failed and Now Showing Currently Upgrading", + url: "/t/update-failed-and-now-showing-currently-upgrading/24332/2", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 6, + name: "support", + color: "CEA9A9", + topic_id: 389, + topic_count: 1781, + created_at: "2013-02-05T22:16:38.672Z", + updated_at: "2015-01-22T18:05:33.572Z", + user_id: 1, + topics_year: 1541, + topics_month: 167, + topics_week: 49, + slug: "support", + description: + "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 12272, + latest_post_id: 94602, + latest_topic_id: 24346, + position: 25, + parent_category_id: null, + posts_year: 10571, + posts_month: 1254, + posts_week: 413, + email_in: null, + email_in_allow_strangers: false, + topics_day: 5, + posts_day: 70, + logo_url: "", + background_url: "", + allow_badges: true, + name_lower: "support", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } }, { - "id":94511, - "cooked":"

Hmm, not sure about that, good odds they will be fixed in iOS 8.1 which is due soon.

", - "created_at":"2015-01-23T02:27:16.786Z", - "title":"Dealing with iOS 8 Mobile Safari bugs?", - "url":"/t/dealing-with-ios-8-mobile-safari-bugs/24101/5", - "user_title":"co-founder", - "user_long_name":"Jeff Atwood", - "category":{ - "id":2, - "name":"feature", - "color":"0E76BD", - "topic_id":11, - "topic_count":1592, - "created_at":"2013-02-02T21:42:52.552Z", - "updated_at":"2015-01-22T18:05:32.647Z", - "user_id":1, - "topics_year":919, - "topics_month":60, - "topics_week":20, - "slug":"feature", - "description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.", - "text_color":"FFFFFF", - "read_restricted":false, - "auto_close_hours":null, - "post_count":14360, - "latest_post_id":94600, - "latest_topic_id":24344, - "position":25, - "parent_category_id":null, - "posts_year":8617, - "posts_month":690, - "posts_week":190, - "email_in":null, - "email_in_allow_strangers":false, - "topics_day":2, - "posts_day":8, - "logo_url":null, - "background_url":null, - "allow_badges":true, - "name_lower":"feature", - "auto_close_based_on_last_post":false + id: 94511, + cooked: + "

Hmm, not sure about that, good odds they will be fixed in iOS 8.1 which is due soon.

", + created_at: "2015-01-23T02:27:16.786Z", + title: "Dealing with iOS 8 Mobile Safari bugs?", + url: "/t/dealing-with-ios-8-mobile-safari-bugs/24101/5", + user_title: "co-founder", + user_long_name: "Jeff Atwood", + category: { + id: 2, + name: "feature", + color: "0E76BD", + topic_id: 11, + topic_count: 1592, + created_at: "2013-02-02T21:42:52.552Z", + updated_at: "2015-01-22T18:05:32.647Z", + user_id: 1, + topics_year: 919, + topics_month: 60, + topics_week: 20, + slug: "feature", + description: + "Discussion about features or potential features of Discourse: how they work, why they work, etc.", + text_color: "FFFFFF", + read_restricted: false, + auto_close_hours: null, + post_count: 14360, + latest_post_id: 94600, + latest_topic_id: 24344, + position: 25, + parent_category_id: null, + posts_year: 8617, + posts_month: 690, + posts_week: 190, + email_in: null, + email_in_allow_strangers: false, + topics_day: 2, + posts_day: 8, + logo_url: null, + background_url: null, + allow_badges: true, + name_lower: "feature", + auto_close_based_on_last_post: false }, - "user":{ - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" + user: { + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png" } } ] diff --git a/test/javascripts/fixtures/groups-fixtures.js.es6 b/test/javascripts/fixtures/groups-fixtures.js.es6 index 79f47e64cb..136f8bcb39 100644 --- a/test/javascripts/fixtures/groups-fixtures.js.es6 +++ b/test/javascripts/fixtures/groups-fixtures.js.es6 @@ -1,3 +1,54 @@ export default { - "/groups.json": {"groups":[{"id":41,"automatic":false,"name":"discourse","user_count":0,"alias_level":0,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"has_messages":false,"flair_url":null,"flair_bg_color":null,"flair_color":null,"bio_raw":"","bio_cooked":null,"public_admission":true,"allow_membership_requests":false,"full_name":"Awesome Team"},{"id":42,"automatic":false,"name":"Macdonald","user_count":0,"alias_level":99,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"has_messages":false,"flair_url":null,"flair_bg_color":null,"flair_color":null,"bio_raw":null,"bio_cooked":null,"public_admission":false,"allow_membership_requests":true,"membership_request_template":"Please add me","full_name":null}],"extras":{"group_user_ids":[]},"total_rows_groups":2,"load_more_groups":"/groups?page=1"} -} + "/groups.json": { + groups: [ + { + id: 41, + automatic: false, + name: "discourse", + user_count: 0, + alias_level: 0, + visible: true, + automatic_membership_email_domains: "", + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: "", + bio_cooked: null, + public_admission: true, + allow_membership_requests: false, + full_name: "Awesome Team" + }, + { + id: 42, + automatic: false, + name: "Macdonald", + user_count: 0, + alias_level: 99, + visible: true, + automatic_membership_email_domains: "", + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + allow_membership_requests: true, + membership_request_template: "Please add me", + full_name: null + } + ], + extras: { group_user_ids: [] }, + total_rows_groups: 2, + load_more_groups: "/groups?page=1" + } +}; diff --git a/test/javascripts/fixtures/new-contributors.js.es6 b/test/javascripts/fixtures/new-contributors.js.es6 index fc3d6f86c9..5b89bc059b 100644 --- a/test/javascripts/fixtures/new-contributors.js.es6 +++ b/test/javascripts/fixtures/new-contributors.js.es6 @@ -1,49 +1,60 @@ export default { "/admin/reports/new_contributors": { - "report": { - "type": "new_contributors", - "title": "New Contributors", - "xaxis": "", - "yaxis": "", - "data": [{ - "x": "2018-04-11", - "y": 10 - }, { - "x": "2018-04-12", - "y": 10 - }, { - "x": "2018-04-13", - "y": 60 - }, { - "x": "2018-04-14", - "y": 60 - }, { - "x": "2018-04-15", - "y": 10 - }, { - "x": "2018-04-16", - "y": 10 - }, { - "x": "2018-04-17", - "y": 10 - }, { - "x": "2018-04-19", - "y": 10 - }, { - "x": "2018-04-18", - "y": 10 - }, { - "x": "2018-04-20", - "y": 1 - }], - "total": 121, - "start_date": "2018-03-26T00:00:00.000Z", - "end_date": "2018-04-25T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": null, - "labels": null, - "report_key": "" + report: { + type: "new_contributors", + title: "New Contributors", + xaxis: "", + yaxis: "", + data: [ + { + x: "2018-04-11", + y: 10 + }, + { + x: "2018-04-12", + y: 10 + }, + { + x: "2018-04-13", + y: 60 + }, + { + x: "2018-04-14", + y: 60 + }, + { + x: "2018-04-15", + y: 10 + }, + { + x: "2018-04-16", + y: 10 + }, + { + x: "2018-04-17", + y: 10 + }, + { + x: "2018-04-19", + y: 10 + }, + { + x: "2018-04-18", + y: 10 + }, + { + x: "2018-04-20", + y: 1 + } + ], + total: 121, + start_date: "2018-03-26T00:00:00.000Z", + end_date: "2018-04-25T23:59:59.999Z", + category_id: null, + group_id: null, + prev30Days: null, + labels: null, + report_key: "" } } }; diff --git a/test/javascripts/fixtures/notification_fixtures.js.es6 b/test/javascripts/fixtures/notification_fixtures.js.es6 index a1b092c503..d9669d9d9b 100644 --- a/test/javascripts/fixtures/notification_fixtures.js.es6 +++ b/test/javascripts/fixtures/notification_fixtures.js.es6 @@ -1,2 +1,16 @@ /*jshint maxlen:10000000 */ -export default {"/notifications": {notifications: [ { id: 123, notification_type: 2, read: false, post_number: 2, topic_id: 1234, slug: "a-slug", data: { topic_title: "some title", display_username: "velesin" } } ] }}; +export default { + "/notifications": { + notifications: [ + { + id: 123, + notification_type: 2, + read: false, + post_number: 2, + topic_id: 1234, + slug: "a-slug", + data: { topic_title: "some title", display_username: "velesin" } + } + ] + } +}; diff --git a/test/javascripts/fixtures/post.js.es6 b/test/javascripts/fixtures/post.js.es6 index 345c20a1be..28288e4739 100644 --- a/test/javascripts/fixtures/post.js.es6 +++ b/test/javascripts/fixtures/post.js.es6 @@ -1,6 +1,157 @@ export default { - "/posts/398": {"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"

Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?

","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"user_title":null,"raw":"Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}, - "/posts/18": {"id":18,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","name":"Evil Trout","uploaded_avatar_id":9,"created_at":"2015-08-13T14:49:11.840Z","cooked":"

This is the first post.

","post_number":1,"post_type":1,"updated_at":"2015-08-13T14:49:11.840Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":9,"topic_slug":"this-is-a-test-topic","display_username":"","primary_group_name":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"user_title":null,"raw":"This is the first post.","actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}, - "/posts/19": {"id":19,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","name":"Evil Trout","uploaded_avatar_id":9,"created_at":"2015-08-13T14:49:18.231Z","cooked":"

This is the second post.

","post_number":2,"post_type":1,"updated_at":"2015-08-13T14:49:18.231Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":9,"topic_slug":"this-is-a-test-topic","display_username":"","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"raw":"This is the second post.","actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false} + "/posts/398": { + id: 398, + name: "Uwe Keim", + username: "uwe_keim", + avatar_template: "/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png", + uploaded_avatar_id: 5697, + created_at: "2013-02-05T21:29:00.280Z", + cooked: + "

Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?

", + post_number: 1, + post_type: 1, + updated_at: "2013-02-05T21:29:00.280Z", + like_count: 0, + reply_count: 1, + reply_to_post_number: null, + quote_count: 0, + avg_time: 25, + incoming_link_count: 314, + reads: 475, + score: 1702.25, + yours: false, + topic_id: 280, + topic_slug: "internationalization-localization", + display_username: "Uwe Keim", + primary_group_name: null, + version: 1, + can_edit: true, + can_delete: false, + can_recover: true, + user_title: null, + raw: + "Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?", + actions_summary: [ + { id: 2, count: 0, hidden: false, can_act: true, can_defer_flags: false }, + { id: 3, count: 0, hidden: false, can_act: true, can_defer_flags: false }, + { id: 4, count: 0, hidden: false, can_act: true, can_defer_flags: false }, + { id: 5, count: 0, hidden: true, can_act: true, can_defer_flags: false }, + { id: 6, count: 0, hidden: false, can_act: true, can_defer_flags: false }, + { id: 7, count: 0, hidden: false, can_act: true, can_defer_flags: false }, + { id: 8, count: 0, hidden: false, can_act: true, can_defer_flags: false } + ], + moderator: false, + admin: false, + staff: false, + user_id: 255, + hidden: false, + hidden_reason_id: null, + trust_level: 2, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false + }, + "/posts/18": { + id: 18, + username: "eviltrout", + avatar_template: + "//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon", + name: "Evil Trout", + uploaded_avatar_id: 9, + created_at: "2015-08-13T14:49:11.840Z", + cooked: "

This is the first post.

", + post_number: 1, + post_type: 1, + updated_at: "2015-08-13T14:49:11.840Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + avg_time: null, + incoming_link_count: 0, + reads: 1, + score: 0, + yours: true, + topic_id: 9, + topic_slug: "this-is-a-test-topic", + display_username: "", + primary_group_name: null, + version: 1, + can_edit: true, + can_delete: false, + can_recover: true, + user_title: null, + raw: "This is the first post.", + actions_summary: [ + { id: 3, can_act: true }, + { id: 4, can_act: true }, + { id: 5, hidden: true, can_act: true }, + { id: 7, can_act: true }, + { id: 8, can_act: true } + ], + moderator: false, + admin: true, + staff: true, + user_id: 1, + hidden: false, + hidden_reason_id: null, + trust_level: 4, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false + }, + "/posts/19": { + id: 19, + username: "eviltrout", + avatar_template: + "//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon", + name: "Evil Trout", + uploaded_avatar_id: 9, + created_at: "2015-08-13T14:49:18.231Z", + cooked: "

This is the second post.

", + post_number: 2, + post_type: 1, + updated_at: "2015-08-13T14:49:18.231Z", + reply_count: 0, + reply_to_post_number: null, + quote_count: 0, + avg_time: null, + incoming_link_count: 0, + reads: 1, + score: 0, + yours: true, + topic_id: 9, + topic_slug: "this-is-a-test-topic", + display_username: "", + primary_group_name: null, + version: 1, + can_edit: true, + can_delete: true, + can_recover: true, + read: true, + user_title: null, + raw: "This is the second post.", + actions_summary: [ + { id: 3, can_act: true }, + { id: 4, can_act: true }, + { id: 5, hidden: true, can_act: true }, + { id: 7, can_act: true }, + { id: 8, can_act: true } + ], + moderator: false, + admin: true, + staff: true, + user_id: 1, + hidden: false, + hidden_reason_id: null, + trust_level: 4, + deleted_at: null, + user_deleted: false, + edit_reason: null, + can_view_edit_history: true, + wiki: false + } }; - diff --git a/test/javascripts/fixtures/posts.js.es6 b/test/javascripts/fixtures/posts.js.es6 index dea8904521..caa70dc337 100644 --- a/test/javascripts/fixtures/posts.js.es6 +++ b/test/javascripts/fixtures/posts.js.es6 @@ -1,19 +1,19 @@ export default { "/admin/reports/posts": { - "report": { - "type": "topics", - "title": "Topics", - "xaxis": "Day", - "yaxis": "Number of new posts", - "data": null, - "total": null, - "start_date": "2018-03-26T00:00:00.000Z", - "end_date": "2018-04-25T23:59:59.999Z", - "category_id": null, - "group_id": null, - "prev30Days": 0, - "labels": null, - "report_key": "" + report: { + type: "topics", + title: "Topics", + xaxis: "Day", + yaxis: "Number of new posts", + data: null, + total: null, + start_date: "2018-03-26T00:00:00.000Z", + end_date: "2018-04-25T23:59:59.999Z", + category_id: null, + group_id: null, + prev30Days: 0, + labels: null, + report_key: "" } } }; diff --git a/test/javascripts/fixtures/problems.js.es6 b/test/javascripts/fixtures/problems.js.es6 index d9f77979fd..7e188ee583 100644 --- a/test/javascripts/fixtures/problems.js.es6 +++ b/test/javascripts/fixtures/problems.js.es6 @@ -1,4 +1,5 @@ export default { "/admin/dashboard/problems.json": { + problems: ["Houston..."] } }; diff --git a/test/javascripts/fixtures/reports.js.es6 b/test/javascripts/fixtures/reports.js.es6 new file mode 100644 index 0000000000..5d818be833 --- /dev/null +++ b/test/javascripts/fixtures/reports.js.es6 @@ -0,0 +1,11 @@ +export default { + "/admin/reports": { + reports: [ + { + title: "My report", + description: "List of my activities", + type: "my_report" + } + ] + } +}; diff --git a/test/javascripts/fixtures/search-fixtures.js.es6 b/test/javascripts/fixtures/search-fixtures.js.es6 index a0b8e22bda..d49a708510 100644 --- a/test/javascripts/fixtures/search-fixtures.js.es6 +++ b/test/javascripts/fixtures/search-fixtures.js.es6 @@ -1,770 +1,818 @@ export default { "/search.json": { - "users":[ + users: [ { - "id":19, - "username":"eviltrout", - "uploaded_avatar_id":5275, - "avatar_template":"/user_avatar/meta.discourse.org/eviltrout/{size}/5275_1.png" + id: 19, + username: "eviltrout", + uploaded_avatar_id: 5275, + avatar_template: + "/user_avatar/meta.discourse.org/eviltrout/{size}/5275_1.png" }, { - "id":8617, - "username":"Mittineague", - "uploaded_avatar_id":40997, - "avatar_template":"/user_avatar/meta.discourse.org/mittineague/{size}/40997_1.png" + id: 8617, + username: "Mittineague", + uploaded_avatar_id: 40997, + avatar_template: + "/user_avatar/meta.discourse.org/mittineague/{size}/40997_1.png" }, { - "id":12662, - "username":"singmajesty", - "uploaded_avatar_id":36342, - "avatar_template":"/user_avatar/meta.discourse.org/singmajesty/{size}/36342_1.png" + id: 12662, + username: "singmajesty", + uploaded_avatar_id: 36342, + avatar_template: + "/user_avatar/meta.discourse.org/singmajesty/{size}/36342_1.png" }, { - "id":6626, - "username":"riking", - "uploaded_avatar_id":40212, - "avatar_template":"/user_avatar/meta.discourse.org/riking/{size}/40212_1.png" + id: 6626, + username: "riking", + uploaded_avatar_id: 40212, + avatar_template: + "/user_avatar/meta.discourse.org/riking/{size}/40212_1.png" }, { - "id":8300, - "username":"cpradio", - "uploaded_avatar_id":4970, - "avatar_template":"/user_avatar/meta.discourse.org/cpradio/{size}/4970_1.png" + id: 8300, + username: "cpradio", + uploaded_avatar_id: 4970, + avatar_template: + "/user_avatar/meta.discourse.org/cpradio/{size}/4970_1.png" }, { - "id":2602, - "username":"georgekaplan59", - "uploaded_avatar_id":31197, - "avatar_template":"/user_avatar/meta.discourse.org/georgekaplan59/{size}/31197_1.png" + id: 2602, + username: "georgekaplan59", + uploaded_avatar_id: 31197, + avatar_template: + "/user_avatar/meta.discourse.org/georgekaplan59/{size}/31197_1.png" }, { - "id":754, - "username":"danneu", - "uploaded_avatar_id":6540, - "avatar_template":"/user_avatar/meta.discourse.org/danneu/{size}/6540_1.png" + id: 754, + username: "danneu", + uploaded_avatar_id: 6540, + avatar_template: + "/user_avatar/meta.discourse.org/danneu/{size}/6540_1.png" }, { - "id":1995, - "username":"zogstrip", - "uploaded_avatar_id":8630, - "avatar_template":"/user_avatar/meta.discourse.org/zogstrip/{size}/8630_1.png" + id: 1995, + username: "zogstrip", + uploaded_avatar_id: 8630, + avatar_template: + "/user_avatar/meta.discourse.org/zogstrip/{size}/8630_1.png" }, { - "id":1, - "username":"sam", - "uploaded_avatar_id":5243, - "avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243_1.png" + id: 1, + username: "sam", + uploaded_avatar_id: 5243, + avatar_template: "/user_avatar/meta.discourse.org/sam/{size}/5243_1.png" }, { - "id":8810, - "username":"fantasticfears", - "uploaded_avatar_id":36351, - "avatar_template":"/user_avatar/meta.discourse.org/fantasticfears/{size}/36351_1.png" + id: 8810, + username: "fantasticfears", + uploaded_avatar_id: 36351, + avatar_template: + "/user_avatar/meta.discourse.org/fantasticfears/{size}/36351_1.png" }, { - "id":14446, - "username":"ladydanger", - "uploaded_avatar_id":null, - "avatar_template":"/letter_avatar/ladydanger/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" + id: 14446, + username: "ladydanger", + uploaded_avatar_id: null, + avatar_template: + "/letter_avatar/ladydanger/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" }, { - "id":14474, - "username":"dnatoli_redbubble", - "uploaded_avatar_id":null, - "avatar_template":"/letter_avatar/dnatoli_redbubble/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" + id: 14474, + username: "dnatoli_redbubble", + uploaded_avatar_id: null, + avatar_template: + "/letter_avatar/dnatoli_redbubble/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" }, { - "id":14514, - "username":"adelsmee", - "uploaded_avatar_id":40445, - "avatar_template":"/user_avatar/meta.discourse.org/adelsmee/{size}/40445_1.png" + id: 14514, + username: "adelsmee", + uploaded_avatar_id: 40445, + avatar_template: + "/user_avatar/meta.discourse.org/adelsmee/{size}/40445_1.png" }, { - "id":32, - "username":"codinghorror", - "uploaded_avatar_id":5297, - "avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297_1.png" + id: 32, + username: "codinghorror", + uploaded_avatar_id: 5297, + avatar_template: + "/user_avatar/meta.discourse.org/codinghorror/{size}/5297_1.png" }, { - "id":14448, - "username":"snjqi188", - "uploaded_avatar_id":null, - "avatar_template":"/letter_avatar/snjqi188/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" + id: 14448, + username: "snjqi188", + uploaded_avatar_id: null, + avatar_template: + "/letter_avatar/snjqi188/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" }, { - "id":14657, - "username":"Alex_Flom", - "uploaded_avatar_id":41037, - "avatar_template":"/user_avatar/meta.discourse.org/alex_flom/{size}/41037_1.png" + id: 14657, + username: "Alex_Flom", + uploaded_avatar_id: 41037, + avatar_template: + "/user_avatar/meta.discourse.org/alex_flom/{size}/41037_1.png" }, { - "id":14353, - "username":"Simon_Cossar", - "uploaded_avatar_id":40130, - "avatar_template":"/user_avatar/meta.discourse.org/simon_cossar/{size}/40130_1.png" + id: 14353, + username: "Simon_Cossar", + uploaded_avatar_id: 40130, + avatar_template: + "/user_avatar/meta.discourse.org/simon_cossar/{size}/40130_1.png" }, { - "id":14184, - "username":"takaminacchan", - "uploaded_avatar_id":39685, - "avatar_template":"/user_avatar/meta.discourse.org/takaminacchan/{size}/39685_1.png" + id: 14184, + username: "takaminacchan", + uploaded_avatar_id: 39685, + avatar_template: + "/user_avatar/meta.discourse.org/takaminacchan/{size}/39685_1.png" }, { - "id":9931, - "username":"Frank", - "uploaded_avatar_id":null, - "avatar_template":"/letter_avatar/frank/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" + id: 9931, + username: "Frank", + uploaded_avatar_id: null, + avatar_template: + "/letter_avatar/frank/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" }, { - "id":8364, - "username":"codetricity", - "uploaded_avatar_id":3773, - "avatar_template":"/user_avatar/meta.discourse.org/codetricity/{size}/3773_1.png" + id: 8364, + username: "codetricity", + uploaded_avatar_id: 3773, + avatar_template: + "/user_avatar/meta.discourse.org/codetricity/{size}/3773_1.png" }, { - "id":4949, - "username":"brodock", - "uploaded_avatar_id":13541, - "avatar_template":"/user_avatar/meta.discourse.org/brodock/{size}/13541_1.png" + id: 4949, + username: "brodock", + uploaded_avatar_id: 13541, + avatar_template: + "/user_avatar/meta.discourse.org/brodock/{size}/13541_1.png" }, { - "id":14, - "username":"clay", - "uploaded_avatar_id":5265, - "avatar_template":"/user_avatar/meta.discourse.org/clay/{size}/5265_1.png" + id: 14, + username: "clay", + uploaded_avatar_id: 5265, + avatar_template: + "/user_avatar/meta.discourse.org/clay/{size}/5265_1.png" }, { - "id":8385, - "username":"zchrykng", - "uploaded_avatar_id":18517, - "avatar_template":"/user_avatar/meta.discourse.org/zchrykng/{size}/18517_1.png" + id: 8385, + username: "zchrykng", + uploaded_avatar_id: 18517, + avatar_template: + "/user_avatar/meta.discourse.org/zchrykng/{size}/18517_1.png" }, { - "id":3520, - "username":"arlyxiao", - "uploaded_avatar_id":11206, - "avatar_template":"/user_avatar/meta.discourse.org/arlyxiao/{size}/11206_1.png" + id: 3520, + username: "arlyxiao", + uploaded_avatar_id: 11206, + avatar_template: + "/user_avatar/meta.discourse.org/arlyxiao/{size}/11206_1.png" }, { - "id":3493, - "username":"richp10", - "uploaded_avatar_id":11160, - "avatar_template":"/user_avatar/meta.discourse.org/richp10/{size}/11160_1.png" + id: 3493, + username: "richp10", + uploaded_avatar_id: 11160, + avatar_template: + "/user_avatar/meta.discourse.org/richp10/{size}/11160_1.png" }, { - "id":2395, - "username":"lookingsideways", - "uploaded_avatar_id":9290, - "avatar_template":"/user_avatar/meta.discourse.org/lookingsideways/{size}/9290_1.png" + id: 2395, + username: "lookingsideways", + uploaded_avatar_id: 9290, + avatar_template: + "/user_avatar/meta.discourse.org/lookingsideways/{size}/9290_1.png" }, { - "id":2477, - "username":"billybonks", - "uploaded_avatar_id":9430, - "avatar_template":"/user_avatar/meta.discourse.org/billybonks/{size}/9430_1.png" + id: 2477, + username: "billybonks", + uploaded_avatar_id: 9430, + avatar_template: + "/user_avatar/meta.discourse.org/billybonks/{size}/9430_1.png" }, { - "id":7301, - "username":"jasonwhat", - "uploaded_avatar_id":null, - "avatar_template":"/letter_avatar/jasonwhat/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" + id: 7301, + username: "jasonwhat", + uploaded_avatar_id: null, + avatar_template: + "/letter_avatar/jasonwhat/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png" }, { - "id":1819, - "username":"stephan", - "uploaded_avatar_id":8327, - "avatar_template":"/user_avatar/meta.discourse.org/stephan/{size}/8327_1.png" + id: 1819, + username: "stephan", + uploaded_avatar_id: 8327, + avatar_template: + "/user_avatar/meta.discourse.org/stephan/{size}/8327_1.png" }, { - "id":2, - "username":"neil", - "uploaded_avatar_id":5245, - "avatar_template":"/user_avatar/meta.discourse.org/neil/{size}/5245_1.png" + id: 2, + username: "neil", + uploaded_avatar_id: 5245, + avatar_template: + "/user_avatar/meta.discourse.org/neil/{size}/5245_1.png" }, { - "id":2471, - "username":"robconery", - "uploaded_avatar_id":9418, - "avatar_template":"/user_avatar/meta.discourse.org/robconery/{size}/9418_1.png" + id: 2471, + username: "robconery", + uploaded_avatar_id: 9418, + avatar_template: + "/user_avatar/meta.discourse.org/robconery/{size}/9418_1.png" } ], - "topic_list":{ - "can_create_topic":false, - "draft":null, - "draft_key":"new_topic", - "draft_sequence":null, - "per_page":30, - "topics":[ + topic_list: { + can_create_topic: false, + draft: null, + draft_key: "new_topic", + draft_sequence: null, + per_page: 30, + topics: [ { - "id":9318, - "title":"Discourse has a new Markdown Parser!", - "fancy_title":"Discourse has a new Markdown Parser!", - "slug":"discourse-has-a-new-markdown-parser", - "posts_count":1, - "reply_count":0, - "highest_post_number":1, - "image_url":null, - "created_at":"2013-08-24T18:08:06.063Z", - "last_posted_at":"2013-08-24T18:08:06.259Z", - "bumped":true, - "bumped_at":"2015-03-09T04:54:43.977Z", - "unseen":false, - "linked_post_number":1, - "pinned":false, - "unpinned":null, - "excerpt":"...0 lines of Javascript code! An inline example Let's say you want to replace all occurances of \"evil trout\" with a link that says \"EVIL TROUT IS AWESOME\": Discourse.Dialect.on(\"register\", function(event) {...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":2645, - "like_count":21, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"eviltrout", - "category_id":7, - "pinned_globally":false, - "posters":[ + id: 9318, + title: "Discourse has a new Markdown Parser!", + fancy_title: "Discourse has a new Markdown Parser!", + slug: "discourse-has-a-new-markdown-parser", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2013-08-24T18:08:06.063Z", + last_posted_at: "2013-08-24T18:08:06.259Z", + bumped: true, + bumped_at: "2015-03-09T04:54:43.977Z", + unseen: false, + linked_post_number: 1, + pinned: false, + unpinned: null, + excerpt: + '...0 lines of Javascript code! An inline example Let\'s say you want to replace all occurances of "evil trout" with a link that says "EVIL TROUT IS AWESOME": Discourse.Dialect.on("register", function(event) {...', + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 2645, + like_count: 21, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 7, + pinned_globally: false, + posters: [ { - "extras":"latest single", - "description":"Original Poster, Most Recent Poster", - "user_id":19 + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 19 } ] }, { - "id":21792, - "title":"Adding custom emoji/emoticons via a plugin", - "fancy_title":"Adding custom emoji/emoticons via a plugin", - "slug":"adding-custom-emoji-emoticons-via-a-plugin", - "posts_count":34, - "reply_count":24, - "highest_post_number":35, - "image_url":null, - "created_at":"2014-11-03T21:48:48.283Z", - "last_posted_at":"2014-12-23T12:45:11.245Z", - "bumped":true, - "bumped_at":"2014-12-23T12:45:11.245Z", - "unseen":false, - "linked_post_number":1, - "pinned":false, - "unpinned":null, - "excerpt":"...plugin that executes the following method to register a new emoji: Discourse.Dialect.registerEmoji('trout', 'http://cdn.eviltrout.com/images/trout-square.jpg'); Here's a sample plugin that adds a :trout: e...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":1260, - "like_count":25, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"cpradio", - "category_id":22, - "pinned_globally":false, - "posters":[ + id: 21792, + title: "Adding custom emoji/emoticons via a plugin", + fancy_title: "Adding custom emoji/emoticons via a plugin", + slug: "adding-custom-emoji-emoticons-via-a-plugin", + posts_count: 34, + reply_count: 24, + highest_post_number: 35, + image_url: null, + created_at: "2014-11-03T21:48:48.283Z", + last_posted_at: "2014-12-23T12:45:11.245Z", + bumped: true, + bumped_at: "2014-12-23T12:45:11.245Z", + unseen: false, + linked_post_number: 1, + pinned: false, + unpinned: null, + excerpt: + "...plugin that executes the following method to register a new emoji: Discourse.Dialect.registerEmoji('trout', 'http://cdn.eviltrout.com/images/trout-square.jpg'); Here's a sample plugin that adds a :trout: e...", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 1260, + like_count: 25, + has_summary: false, + archetype: "regular", + last_poster_username: "cpradio", + category_id: 22, + pinned_globally: false, + posters: [ { - "extras":null, - "description":"Original Poster", - "user_id":19 + extras: null, + description: "Original Poster", + user_id: 19 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":8617 + extras: null, + description: "Frequent Poster", + user_id: 8617 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":12662 + extras: null, + description: "Frequent Poster", + user_id: 12662 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":6626 + extras: null, + description: "Frequent Poster", + user_id: 6626 }, { - "extras":"latest", - "description":"Most Recent Poster", - "user_id":8300 + extras: "latest", + description: "Most Recent Poster", + user_id: 8300 } ] }, { - "id":3071, - "title":"Would it be possible to make Slug localizable?", - "fancy_title":"Would it be possible to make Slug localizable?", - "slug":"would-it-be-possible-to-make-slug-localizable", - "posts_count":12, - "reply_count":7, - "highest_post_number":12, - "image_url":null, - "created_at":"2013-02-14T11:48:21.474Z", - "last_posted_at":"2014-09-18T14:38:59.064Z", - "bumped":true, - "bumped_at":"2014-09-18T14:38:59.064Z", - "unseen":false, - "linked_post_number":10, - "pinned":false, - "unpinned":null, - "excerpt":"...in `block (2 levels) in < top (required) > ' 3) Slug replaces symbols Failure/Error: Slug.for('evil#trout').should == 'evil-trout' expected: \"evil-trout\" got: \"evil-number-trout\" (using ==) # ./spec/compon...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":766, - "like_count":5, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"fantasticfears", - "category_id":17, - "pinned_globally":false, - "posters":[ + id: 3071, + title: "Would it be possible to make Slug localizable?", + fancy_title: "Would it be possible to make Slug localizable?", + slug: "would-it-be-possible-to-make-slug-localizable", + posts_count: 12, + reply_count: 7, + highest_post_number: 12, + image_url: null, + created_at: "2013-02-14T11:48:21.474Z", + last_posted_at: "2014-09-18T14:38:59.064Z", + bumped: true, + bumped_at: "2014-09-18T14:38:59.064Z", + unseen: false, + linked_post_number: 10, + pinned: false, + unpinned: null, + excerpt: + "...in `block (2 levels) in < top (required) > ' 3) Slug replaces symbols Failure/Error: Slug.for('evil#trout').should == 'evil-trout' expected: \"evil-trout\" got: \"evil-number-trout\" (using ==) # ./spec/compon...", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 766, + like_count: 5, + has_summary: false, + archetype: "regular", + last_poster_username: "fantasticfears", + category_id: 17, + pinned_globally: false, + posters: [ { - "extras":null, - "description":"Original Poster", - "user_id":2602 + extras: null, + description: "Original Poster", + user_id: 2602 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":754 + extras: null, + description: "Frequent Poster", + user_id: 754 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":1995 + extras: null, + description: "Frequent Poster", + user_id: 1995 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":1 + extras: null, + description: "Frequent Poster", + user_id: 1 }, { - "extras":"latest", - "description":"Most Recent Poster", - "user_id":8810 + extras: "latest", + description: "Most Recent Poster", + user_id: 8810 } ] }, { - "id":26875, - "title":"Rails Girls SoC Banter", - "fancy_title":"Rails Girls SoC Banter", - "slug":"rails-girls-soc-banter", - "posts_count":48, - "reply_count":30, - "highest_post_number":48, - "image_url":null, - "created_at":"2015-03-27T11:26:09.903Z", - "last_posted_at":"2015-07-13T23:11:31.481Z", - "bumped":true, - "bumped_at":"2015-07-13T23:11:31.481Z", - "unseen":false, - "linked_post_number":42, - "pinned":false, - "unpinned":null, - "excerpt":"...e inserted by plugins. ## Usage If you handlebars template has: ```handlebars {{plugin-outlet \"evil-trout\"}} ``` Then any handlebars files you create in the `connectors/evil-trout` directory will automatic...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":1224, - "like_count":81, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"snjqi188", - "category_id":7, - "pinned_globally":false, - "posters":[ + id: 26875, + title: "Rails Girls SoC Banter", + fancy_title: "Rails Girls SoC Banter", + slug: "rails-girls-soc-banter", + posts_count: 48, + reply_count: 30, + highest_post_number: 48, + image_url: null, + created_at: "2015-03-27T11:26:09.903Z", + last_posted_at: "2015-07-13T23:11:31.481Z", + bumped: true, + bumped_at: "2015-07-13T23:11:31.481Z", + unseen: false, + linked_post_number: 42, + pinned: false, + unpinned: null, + excerpt: + '...e inserted by plugins. ## Usage If you handlebars template has: ```handlebars {{plugin-outlet "evil-trout"}} ``` Then any handlebars files you create in the `connectors/evil-trout` directory will automatic...', + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 1224, + like_count: 81, + has_summary: false, + archetype: "regular", + last_poster_username: "snjqi188", + category_id: 7, + pinned_globally: false, + posters: [ { - "extras":null, - "description":"Original Poster", - "user_id":14446 + extras: null, + description: "Original Poster", + user_id: 14446 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":14474 + extras: null, + description: "Frequent Poster", + user_id: 14474 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":14514 + extras: null, + description: "Frequent Poster", + user_id: 14514 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":32 + extras: null, + description: "Frequent Poster", + user_id: 32 }, { - "extras":"latest", - "description":"Most Recent Poster", - "user_id":14448 + extras: "latest", + description: "Most Recent Poster", + user_id: 14448 } ] }, { - "id":31001, - "title":"Beginner's Guide to Creating Discourse Plugins Part 2: Plugin Outlets", - "fancy_title":"Beginner’s Guide to Creating Discourse Plugins Part 2: Plugin Outlets", - "slug":"beginners-guide-to-creating-discourse-plugins-part-2-plugin-outlets", - "posts_count":1, - "reply_count":0, - "highest_post_number":1, - "image_url":null, - "created_at":"2015-07-12T17:48:27.322Z", - "last_posted_at":"2015-07-12T17:48:27.403Z", - "bumped":true, - "bumped_at":"2015-07-13T04:18:14.901Z", - "unseen":false, - "linked_post_number":1, - "pinned":false, - "unpinned":null, - "excerpt":"...nectors/ < outlet name > in it. For example, if your handlebars template has: {{plugin-outlet \"evil-trout\"}} Then any handlebars files you create in the connectors/evil-trout directory will automatically b...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":220, - "like_count":16, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"eviltrout", - "category_id":10, - "pinned_globally":false, - "posters":[ + id: 31001, + title: + "Beginner's Guide to Creating Discourse Plugins Part 2: Plugin Outlets", + fancy_title: + "Beginner’s Guide to Creating Discourse Plugins Part 2: Plugin Outlets", + slug: + "beginners-guide-to-creating-discourse-plugins-part-2-plugin-outlets", + posts_count: 1, + reply_count: 0, + highest_post_number: 1, + image_url: null, + created_at: "2015-07-12T17:48:27.322Z", + last_posted_at: "2015-07-12T17:48:27.403Z", + bumped: true, + bumped_at: "2015-07-13T04:18:14.901Z", + unseen: false, + linked_post_number: 1, + pinned: false, + unpinned: null, + excerpt: + '...nectors/ < outlet name > in it. For example, if your handlebars template has: {{plugin-outlet "evil-trout"}} Then any handlebars files you create in the connectors/evil-trout directory will automatically b...', + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 220, + like_count: 16, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 10, + pinned_globally: false, + posters: [ { - "extras":"latest single", - "description":"Original Poster, Most Recent Poster", - "user_id":19 + extras: "latest single", + description: "Original Poster, Most Recent Poster", + user_id: 19 } ] }, { - "id":29176, - "title":"How can I add some custom html to the bottom of the categories page?", - "fancy_title":"How can I add some custom html to the bottom of the categories page?", - "slug":"how-can-i-add-some-custom-html-to-the-bottom-of-the-categories-page", - "posts_count":12, - "reply_count":10, - "highest_post_number":13, - "image_url":null, - "created_at":"2015-05-23T19:08:35.447Z", - "last_posted_at":"2015-05-25T08:16:25.989Z", - "bumped":true, - "bumped_at":"2015-05-25T08:16:25.989Z", - "unseen":false, - "linked_post_number":12, - "pinned":false, - "unpinned":null, - "excerpt":"...e inserted by plugins. ## Usage If you handlebars template has: ```handlebars {{plugin-outlet \"evil-trout\"}} ``` Then any handlebars files you create in the `connectors/evil-trout` directory will automatic...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":190, - "like_count":8, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"sam", - "category_id":6, - "pinned_globally":false, - "posters":[ + id: 29176, + title: + "How can I add some custom html to the bottom of the categories page?", + fancy_title: + "How can I add some custom html to the bottom of the categories page?", + slug: + "how-can-i-add-some-custom-html-to-the-bottom-of-the-categories-page", + posts_count: 12, + reply_count: 10, + highest_post_number: 13, + image_url: null, + created_at: "2015-05-23T19:08:35.447Z", + last_posted_at: "2015-05-25T08:16:25.989Z", + bumped: true, + bumped_at: "2015-05-25T08:16:25.989Z", + unseen: false, + linked_post_number: 12, + pinned: false, + unpinned: null, + excerpt: + '...e inserted by plugins. ## Usage If you handlebars template has: ```handlebars {{plugin-outlet "evil-trout"}} ``` Then any handlebars files you create in the `connectors/evil-trout` directory will automatic...', + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 190, + like_count: 8, + has_summary: false, + archetype: "regular", + last_poster_username: "sam", + category_id: 6, + pinned_globally: false, + posters: [ { - "extras":null, - "description":"Original Poster", - "user_id":14657 + extras: null, + description: "Original Poster", + user_id: 14657 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":6626 + extras: null, + description: "Frequent Poster", + user_id: 6626 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":8617 + extras: null, + description: "Frequent Poster", + user_id: 8617 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":14353 + extras: null, + description: "Frequent Poster", + user_id: 14353 }, { - "extras":"latest", - "description":"Most Recent Poster", - "user_id":1 + extras: "latest", + description: "Most Recent Poster", + user_id: 1 } ] }, { - "id":26192, - "title":"403 when embedding a DigitalOcean droplet", - "fancy_title":"403 when embedding a DigitalOcean droplet", - "slug":"403-when-embedding-a-digital-ocean-droplet", - "posts_count":7, - "reply_count":3, - "highest_post_number":7, - "image_url":null, - "created_at":"2015-03-10T21:22:19.206Z", - "last_posted_at":"2015-03-11T22:31:04.520Z", - "bumped":true, - "bumped_at":"2015-03-11T22:31:04.520Z", - "unseen":false, - "linked_post_number":4, - "pinned":false, - "unpinned":null, - "excerpt":"Yes I am Robin as well as Evil Trout smile :smile: If you followed those instructions and are getting access errors, you might want to d...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":232, - "like_count":2, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"codinghorror", - "category_id":6, - "pinned_globally":false, - "posters":[ + id: 26192, + title: "403 when embedding a DigitalOcean droplet", + fancy_title: "403 when embedding a DigitalOcean droplet", + slug: "403-when-embedding-a-digital-ocean-droplet", + posts_count: 7, + reply_count: 3, + highest_post_number: 7, + image_url: null, + created_at: "2015-03-10T21:22:19.206Z", + last_posted_at: "2015-03-11T22:31:04.520Z", + bumped: true, + bumped_at: "2015-03-11T22:31:04.520Z", + unseen: false, + linked_post_number: 4, + pinned: false, + unpinned: null, + excerpt: + "Yes I am Robin as well as Evil Trout smile :smile: If you followed those instructions and are getting access errors, you might want to d...", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 232, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "codinghorror", + category_id: 6, + pinned_globally: false, + posters: [ { - "extras":null, - "description":"Original Poster", - "user_id":14184 + extras: null, + description: "Original Poster", + user_id: 14184 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":19 + extras: null, + description: "Frequent Poster", + user_id: 19 }, { - "extras":"latest", - "description":"Most Recent Poster", - "user_id":32 + extras: "latest", + description: "Most Recent Poster", + user_id: 32 } ] }, { - "id":20883, - "title":"S3 competitor integration", - "fancy_title":"S3 competitor integration", - "slug":"s3-competitor-integration", - "posts_count":3, - "reply_count":1, - "highest_post_number":3, - "image_url":"https://discourse-cdn.global.ssl.fastly.net/meta/images/emoji/twitter/smile.png?v=1", - "created_at":"2014-10-07T13:37:19.628Z", - "last_posted_at":"2014-10-07T18:46:22.493Z", - "bumped":true, - "bumped_at":"2014-10-07T18:46:22.493Z", - "unseen":false, - "linked_post_number":3, - "pinned":false, - "unpinned":null, - "excerpt":"I have seem some of your testing 'stuff' (evil trout's actually). And it looks like a HUUUUUUUUGGGE time sink (ice pick to the eyeballs). but...I believ...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":141, - "like_count":2, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"Frank", - "category_id":2, - "pinned_globally":false, - "posters":[ + id: 20883, + title: "S3 competitor integration", + fancy_title: "S3 competitor integration", + slug: "s3-competitor-integration", + posts_count: 3, + reply_count: 1, + highest_post_number: 3, + image_url: + "https://discourse-cdn.global.ssl.fastly.net/meta/images/emoji/twitter/smile.png?v=1", + created_at: "2014-10-07T13:37:19.628Z", + last_posted_at: "2014-10-07T18:46:22.493Z", + bumped: true, + bumped_at: "2014-10-07T18:46:22.493Z", + unseen: false, + linked_post_number: 3, + pinned: false, + unpinned: null, + excerpt: + "I have seem some of your testing 'stuff' (evil trout's actually). And it looks like a HUUUUUUUUGGGE time sink (ice pick to the eyeballs). but...I believ...", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 141, + like_count: 2, + has_summary: false, + archetype: "regular", + last_poster_username: "Frank", + category_id: 2, + pinned_globally: false, + posters: [ { - "extras":"latest", - "description":"Original Poster, Most Recent Poster", - "user_id":9931 + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 9931 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":1995 + extras: null, + description: "Frequent Poster", + user_id: 1995 } ] }, { - "id":13534, - "title":"Blogging Platforms, Ghost, and Discourse", - "fancy_title":"Blogging Platforms, Ghost, and Discourse", - "slug":"blogging-platforms-ghost-and-discourse", - "posts_count":18, - "reply_count":13, - "highest_post_number":18, - "image_url":null, - "created_at":"2014-03-08T15:46:35.174Z", - "last_posted_at":"2014-03-26T18:25:45.895Z", - "bumped":true, - "bumped_at":"2014-03-26T18:25:45.895Z", - "unseen":false, - "linked_post_number":1, - "pinned":false, - "unpinned":null, - "excerpt":"...urse, do you mean that the blog comments for Ghost will be driven by Discourse, similar to the Evil Trout blog ? What about using Discourse as the blog platform itself, not as the comment engine at the end...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":2182, - "like_count":17, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"codetricity", - "category_id":17, - "pinned_globally":false, - "posters":[ + id: 13534, + title: "Blogging Platforms, Ghost, and Discourse", + fancy_title: "Blogging Platforms, Ghost, and Discourse", + slug: "blogging-platforms-ghost-and-discourse", + posts_count: 18, + reply_count: 13, + highest_post_number: 18, + image_url: null, + created_at: "2014-03-08T15:46:35.174Z", + last_posted_at: "2014-03-26T18:25:45.895Z", + bumped: true, + bumped_at: "2014-03-26T18:25:45.895Z", + unseen: false, + linked_post_number: 1, + pinned: false, + unpinned: null, + excerpt: + "...urse, do you mean that the blog comments for Ghost will be driven by Discourse, similar to the Evil Trout blog ? What about using Discourse as the blog platform itself, not as the comment engine at the end...", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 2182, + like_count: 17, + has_summary: false, + archetype: "regular", + last_poster_username: "codetricity", + category_id: 17, + pinned_globally: false, + posters: [ { - "extras":"latest", - "description":"Original Poster, Most Recent Poster", - "user_id":8364 + extras: "latest", + description: "Original Poster, Most Recent Poster", + user_id: 8364 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":4949 + extras: null, + description: "Frequent Poster", + user_id: 4949 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":14 + extras: null, + description: "Frequent Poster", + user_id: 14 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":32 + extras: null, + description: "Frequent Poster", + user_id: 32 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":8385 + extras: null, + description: "Frequent Poster", + user_id: 8385 } ] }, { - "id":4859, - "title":"All of the site functions based on ajax?", - "fancy_title":"All of the site functions based on ajax?", - "slug":"all-of-the-site-functions-based-on-ajax", - "posts_count":28, - "reply_count":20, - "highest_post_number":28, - "image_url":null, - "created_at":"2013-03-18T08:59:46.135Z", - "last_posted_at":"2013-10-18T20:22:30.677Z", - "bumped":true, - "bumped_at":"2013-10-18T20:22:30.677Z", - "unseen":false, - "linked_post_number":21, - "pinned":false, - "unpinned":null, - "excerpt":"please see evil trouts blog post http://eviltrout.com/2013/02/27/adding-to-discourse-part-1.html", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":1629, - "like_count":17, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"jasonwhat", - "category_id":17, - "pinned_globally":false, - "posters":[ + id: 4859, + title: "All of the site functions based on ajax?", + fancy_title: "All of the site functions based on ajax?", + slug: "all-of-the-site-functions-based-on-ajax", + posts_count: 28, + reply_count: 20, + highest_post_number: 28, + image_url: null, + created_at: "2013-03-18T08:59:46.135Z", + last_posted_at: "2013-10-18T20:22:30.677Z", + bumped: true, + bumped_at: "2013-10-18T20:22:30.677Z", + unseen: false, + linked_post_number: 21, + pinned: false, + unpinned: null, + excerpt: + "please see evil trouts blog post http://eviltrout.com/2013/02/27/adding-to-discourse-part-1.html", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 1629, + like_count: 17, + has_summary: false, + archetype: "regular", + last_poster_username: "jasonwhat", + category_id: 17, + pinned_globally: false, + posters: [ { - "extras":null, - "description":"Original Poster", - "user_id":3520 + extras: null, + description: "Original Poster", + user_id: 3520 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":3493 + extras: null, + description: "Frequent Poster", + user_id: 3493 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":2395 + extras: null, + description: "Frequent Poster", + user_id: 2395 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":2477 + extras: null, + description: "Frequent Poster", + user_id: 2477 }, { - "extras":"latest", - "description":"Most Recent Poster", - "user_id":7301 + extras: "latest", + description: "Most Recent Poster", + user_id: 7301 } ] }, { - "id":7220, - "title":"Javascript dependencies", - "fancy_title":"Javascript dependencies", - "slug":"javascript-dependencies", - "posts_count":8, - "reply_count":5, - "highest_post_number":8, - "image_url":null, - "created_at":"2013-06-06T11:11:18.522Z", - "last_posted_at":"2013-06-07T18:43:51.449Z", - "bumped":true, - "bumped_at":"2013-06-07T18:43:51.449Z", - "unseen":false, - "linked_post_number":3, - "pinned":false, - "unpinned":null, - "excerpt":"...ould be in vendor directory of one of the gems Ahh I need to look at Gemfile Ahh I need to use Evil Trouts bundle open handlebars trick. I am completely against this new best practice, its inconsistent wit...", - "visible":true, - "closed":false, - "archived":false, - "bookmarked":null, - "liked":null, - "views":1010, - "like_count":0, - "has_summary":false, - "archetype":"regular", - "last_poster_username":"eviltrout", - "category_id":7, - "pinned_globally":false, - "posters":[ + id: 7220, + title: "Javascript dependencies", + fancy_title: "Javascript dependencies", + slug: "javascript-dependencies", + posts_count: 8, + reply_count: 5, + highest_post_number: 8, + image_url: null, + created_at: "2013-06-06T11:11:18.522Z", + last_posted_at: "2013-06-07T18:43:51.449Z", + bumped: true, + bumped_at: "2013-06-07T18:43:51.449Z", + unseen: false, + linked_post_number: 3, + pinned: false, + unpinned: null, + excerpt: + "...ould be in vendor directory of one of the gems Ahh I need to look at Gemfile Ahh I need to use Evil Trouts bundle open handlebars trick. I am completely against this new best practice, its inconsistent wit...", + visible: true, + closed: false, + archived: false, + bookmarked: null, + liked: null, + views: 1010, + like_count: 0, + has_summary: false, + archetype: "regular", + last_poster_username: "eviltrout", + category_id: 7, + pinned_globally: false, + posters: [ { - "extras":null, - "description":"Original Poster", - "user_id":1819 + extras: null, + description: "Original Poster", + user_id: 1819 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":2 + extras: null, + description: "Frequent Poster", + user_id: 2 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":1 + extras: null, + description: "Frequent Poster", + user_id: 1 }, { - "extras":null, - "description":"Frequent Poster", - "user_id":2471 + extras: null, + description: "Frequent Poster", + user_id: 2471 }, { - "extras":"latest", - "description":"Most Recent Poster", - "user_id":19 + extras: "latest", + description: "Most Recent Poster", + user_id: 19 } ] } @@ -772,668 +820,684 @@ export default { } }, "search/query": { -   "posts": [ -     { -       "id": 3833, -       "name": "Bill Dudney", -       "username": "bdudney", -       "avatar_template": "/user_avatar/meta.discourse.org/bdudney/{size}/8343_1.png", -       "uploaded_avatar_id": 8343, -       "created_at": "2013-02-07T17:46:57.469Z", -       "cooked": "

I've gotten vagrant up and running with a development environment but it's taking forever to load.<\/p>\n\n

For example http://192.168.10.200:3000/<\/a> takes tens of seconds to load.<\/p>\n\n

I'm running the whole stack on a new rMBP with OS X 10.8.2.<\/p>\n\n

Any ideas of what I've done wrong? Or is this just a function of being on the bleeding edge?<\/p>\n\n

Thanks,<\/p>\n\n

-bd<\/p>", -       "post_number": 1, -       "post_type": 1, -       "updated_at": "2013-02-07T17:46:57.469Z", -       "like_count": 0, -       "reply_count": 1, -       "reply_to_post_number": null, -       "quote_count": 0, -       "avg_time": 24, -       "incoming_link_count": 4422, -       "reads": 327, -       "score": 21978.4, -       "yours": false, -       "topic_id": 2179, -       "topic_slug": "development-mode-super-slow", -       "display_username": "Bill Dudney", -       "primary_group_name": null, -       "version": 2, -       "can_edit": false, -       "can_delete": false, -       "can_recover": false, -       "user_title": null, -       "actions_summary": [ -         { -           "id": 2, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 3, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 4, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 5, -           "count": 0, -           "hidden": true, -           "can_act": false -         }, -         { -           "id": 6, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 7, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 8, -           "count": 0, -           "hidden": false, -           "can_act": false -         } -       ], -       "moderator": false, -       "admin": false, -       "staff": false, -       "user_id": 1828, -       "hidden": false, -       "hidden_reason_id": null, -       "trust_level": 1, -       "deleted_at": null, -       "user_deleted": false, -       "edit_reason": null, -       "can_view_edit_history": true, -       "wiki": false, -       "blurb": "I've gotten vagrant up and running with a development environment but it's taking forever to load. For example http://192.168.10.200:3000/ takes..." -     }, -     { -       "id": 48887, -       "name": "Arpit Jalan", -       "username": "techAPJ", -       "avatar_template": "/user_avatar/meta.discourse.org/techapj/{size}/3281_1.png", -       "uploaded_avatar_id": 3281, -       "created_at": "2014-04-12T22:22:07.930Z", -       "cooked": "

So you want to set up Discourse on Ubuntu to hack on and develop with?<\/p>\n\n

We'll assume that you don't have Ruby/Rails/Postgre/Redis installed on your Ubuntu system. Let's begin!<\/p>\n\n

Although this guide assumes that you are using Ubuntu, but the set-up instructions will work fine for any Debian based ditribution.<\/em><\/p>\n\n

(If you want to install Discourse for production use, see our install guide<\/a>)<\/em><\/p>\n\n

Install Discourse Dependencies<\/h2>\n\n

Run this script<\/a> in terminal, to setup Rails development environment:<\/p>\n\n

bash <(wget -qO- https://raw.githubusercontent.com/techAPJ/install-rails/master/linux)<\/code><\/pre>\n\n

\nlinux_script.png<\/span>770x211 9.62 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

This will install following new packages on your system:<\/p>\n\n

    \n
  • Git<\/a><\/li>\n
  • rbenv<\/a><\/li>\n
  • ruby-build<\/a><\/li>\n
  • \nRuby<\/a> (stable)<\/li>\n
  • Rails<\/a><\/li>\n
  • PostgreSQL<\/a><\/li>\n
  • SQLite<\/a><\/li>\n
  • Redis<\/a><\/li>\n
  • Bundler<\/a><\/li>\n
  • ImageMagick<\/a><\/li>\n<\/ul>\n\n

    Install Phantomjs:<\/p>\n\n

    For 32 bit macine:<\/p>\n\n

    cd /usr/local/share\nsudo wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.8-linux-i686.tar.bz2\nsudo tar xvf phantomjs-1.9.8-linux-i686.tar.bz2\nsudo rm phantomjs-1.9.8-linux-i686.tar.bz2\nsudo ln -s /usr/local/share/phantomjs-1.9.8-linux-i686/bin/phantomjs /usr/local/bin/phantomjs\ncd<\/code><\/pre>\n\n

    For 64 bit machine:<\/p>\n\n

    cd /usr/local/share\nsudo wget https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-1.9.8-linux-x86_64.tar.bz2\nsudo tar xvf phantomjs-1.9.8-linux-x86_64.tar.bz2\nsudo rm phantomjs-1.9.8-linux-x86_64.tar.bz2\nsudo ln -s /usr/local/share/phantomjs-1.9.8-linux-x86_64/bin/phantomjs /usr/local/bin/phantomjs\ncd<\/code><\/pre>\n\n

    \nphantomjs.png<\/span>969x171 10.1 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    In case you have any of this package pre-installed and don't want to run entire script, see the script<\/a> and pick the packages you don't have currently installed. The script is fine-tuned for Discourse, and includes all the packages required for Discourse installation.<\/em><\/p>\n\n

    Now that we have installed Discourse dependencies, let's move on to install Discourse itself.<\/p>\n\n

    Clone Discourse<\/h2>\n\n

    Clone the Discourse repository in ~/discourse<\/code> folder:<\/p>\n\n

    git clone https://github.com/discourse/discourse.git ~/discourse<\/code><\/pre>\n\n

    \ngit_clone.png<\/span>967x137 7.73 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Setup Database<\/h2>\n\n

    Open psql prompt as postgre user<\/p>\n\n

    sudo -u postgres psql postgres<\/code><\/pre>\n\n

    \npg.png<\/span>725x187 5.79 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Create role with the same name as your ubuntu system username<\/strong> with discourse<\/em> as password:<\/p>\n\n

    CREATE ROLE discourse WITH LOGIN ENCRYPTED PASSWORD 'discourse' CREATEDB SUPERUSER;<\/code><\/pre>\n\n

    In the above command, I named the role as discourse<\/strong>, this means that my ubuntu system username is discourse<\/strong>. (It is necessary for role name to be same as system username, otherwise migrations will not run<\/em>)<\/p>\n\n

    Check that you have successfully created discourse<\/strong> role:<\/p>\n\n

    \\du<\/code><\/pre>\n\n

    \npg_user.png<\/span>725x185 7.5 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Create discourse_development<\/strong> and discourse_test<\/strong> database:<\/p>\n\n

    CREATE DATABASE discourse_development WITH OWNER discourse ENCODING 'UTF8' TEMPLATE template0;\nCREATE DATABASE discourse_test WITH OWNER discourse ENCODING 'UTF8' TEMPLATE template0;<\/code><\/pre>\n\n

    \npg_db.png<\/span>724x143 6.82 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Exit psql prompt by pressing ctrl<\/kbd>d<\/kbd><\/p>\n\n

    Now access psql prompt in discourse_development<\/strong> database as discourse<\/strong> user:<\/p>\n\n

    psql -d discourse_development -U discourse -h localhost<\/code><\/pre>\n\n

    When prompted for password, provide the password which you set at the time of creating role, if you followed the guide as is, the password is discourse<\/strong><\/p>\n\n

    Run following commands, separately:<\/p>\n\n

    CREATE EXTENSION pg_trgm;\nCREATE EXTENSION hstore;<\/code><\/pre>\n\n

    \npg_dev.png<\/span>726x316 13.4 KB<\/span><\/span>\n<\/div><\/a><\/div><\/p>\n\n

    Exit psql prompt by pressing ctrl<\/kbd>d<\/kbd><\/p>\n\n

    Now access psql prompt in discourse_test<\/strong> database as discourse<\/strong> user:<\/p>\n\n

    psql -d discourse_test -U discourse -h localhost<\/code><\/pre>\n\n

    When prompted for password, provide the password which you set at the time of creating role, if you followed the guide as is, the password is discourse<\/strong><\/p>\n\n

    Run following commands, separately:<\/p>\n\n

    CREATE EXTENSION pg_trgm;\nCREATE EXTENSION hstore;<\/code><\/pre>\n\n

    \npg_test.png<\/span>726x318 12.9 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Exit psql prompt by pressing ctrl<\/kbd>d<\/kbd><\/p>\n\n

    You have set-up the database successfully!<\/p>\n\n

    Bootstrap Discourse<\/h2>\n\n

    Switch to your Discourse folder:<\/p>\n\n

    cd ~/discourse<\/code><\/pre>\n\n

    Install the needed gems<\/p>\n\n

    bundle install<\/code><\/pre>\n\n

    \nbundle.png<\/span>724x248 9.75 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Now that you have successfully configured database connection, run this command:<\/p>\n\n

    bundle exec rake db:migrate db:test:prepare db:seed_fu<\/code><\/pre>\n\n

    Now, try running the specs: <\/p>\n\n

    bundle exec rake autospec<\/code><\/pre>\n\n

    \nspecs.png<\/span>717x263 8.63 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Start rails server:<\/p>\n\n

    bundle exec rails server<\/code><\/pre>\n\n

    \nserver.png<\/span>724x229 10.8 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    You should now be able to connect to discourse app on http://localhost:3000<\/a> - try it out!<\/p>\n\n

    \ndiscourse_start.png<\/span>1919x525 20.3 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Configure Mail and Create New Account<\/h2>\n\n

    We will use MailCatcher<\/a> to serve emails in development environment. Install and run MailCatcher:<\/p>\n\n

    gem install mailcatcher\nmailcatcher --http-ip 0.0.0.0<\/code><\/pre>\n\n

    Create new account:<\/p>\n\n

    \ncreate_account.png<\/span>720x401 13.5 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Check confirmation email by going to MailCatcher web interface at http://localhost:1080/<\/a><\/p>\n\n

    \nmc_sign_up_email.png<\/span>1919x480 21.5 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    If you did not receive the email, try running this in console<\/em>: bundle exec sidekiq -q default<\/code><\/p>\n\n

    Click the confirmation link and your account will be activated!<\/p>\n\n

    \ndisc_normal_acc.png<\/span>1919x430 21.8 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Access Admin<\/h2>\n\n

    Now, to make your account as admin, run the following commands in rails console:<\/p>\n\n

    RAILS_ENV=development bundle exec rails c\nu = User.last\nu.admin = true\nu.save<\/code><\/pre>\n\n

    \nadmin_console.png<\/span>722x462 31.7 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Once you execute the above commands successfully, check out your Discourse account again:<\/p>\n\n

    \nadmin_success.png<\/span>1919x1032 30.3 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

    Congratulations! You are now the admin of your own Discourse installation!<\/p>\n\n

    Happy hacking!<\/p>\n\n

    If anything needs to be improved in this guide, feel free to ask on meta.discourse.org<\/a>, or even better, submit a pull request<\/a>.<\/p>", -       "post_number": 1, -       "post_type": 1, -       "updated_at": "2015-06-22T17:24:20.607Z", -       "like_count": 15, -       "reply_count": 2, -       "reply_to_post_number": null, -       "quote_count": 0, -       "avg_time": 36, -       "incoming_link_count": 4680, -       "reads": 491, -       "score": 23815.8, -       "yours": false, -       "topic_id": 14727, -       "topic_slug": "beginners-guide-to-install-discourse-on-ubuntu-for-development", -       "display_username": "Arpit Jalan", -       "primary_group_name": null, -       "version": 26, -       "can_edit": false, -       "can_delete": false, -       "can_recover": false, -       "user_title": "team", -       "actions_summary": [ -         { -           "id": 2, -           "count": 15, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 3, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 4, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 5, -           "count": 0, -           "hidden": true, -           "can_act": false -         }, -         { -           "id": 6, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 7, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 8, -           "count": 0, -           "hidden": false, -           "can_act": false -         } -       ], -       "moderator": true, -       "admin": true, -       "staff": true, -       "user_id": 8222, -       "hidden": false, -       "hidden_reason_id": null, -       "trust_level": 4, -       "deleted_at": null, -       "user_deleted": false, -       "edit_reason": null, -       "can_view_edit_history": true, -       "wiki": true, -       "blurb": "So you want to set up Discourse on Ubuntu to hack on and develop with? We'll assume that you don't have Ruby/Rails/Postgre/Redis installed on your Ubuntu system..." -     }, -     { -       "id": 53437, -       "name": "Arpit Jalan", -       "username": "techAPJ", -       "avatar_template": "/user_avatar/meta.discourse.org/techapj/{size}/3281_1.png", -       "uploaded_avatar_id": 3281, -       "created_at": "2014-05-19T16:59:51.082Z", -       "cooked": "

    So you want to set up Discourse on Mac OS X to hack on and develop with?<\/p>\n\n

    We'll assume that you don't have Ruby/Rails/Postgre/Redis installed on your Mac. Let's begin!<\/p>\n\n

    (If you want to install Discourse for production use, see our install guide<\/a>)<\/em><\/p>\n\n

    Install Discourse Dependencies<\/h2>\n\n

    Run this script<\/a> in terminal, to setup Rails development environment:<\/p>\n\n

    bash <(curl -s https://raw.githubusercontent.com/techAPJ/install-rails/master/mac)<\/code><\/pre>\n\n

    This script will install following new packages on your system:<\/p>\n\n

      \n
    • Git<\/a><\/li>\n
    • rbenv<\/a><\/li>\n
    • ruby-build<\/a><\/li>\n
    • \nRuby<\/a> (latest stable)<\/li>\n
    • Rails<\/a><\/li>\n
    • PostgreSQL<\/a><\/li>\n
    • Redis<\/a><\/li>\n
    • Bundler<\/a><\/li>\n
    • ImageMagick<\/a><\/li>\n
    • PhantomJS<\/a><\/li>\n<\/ul>\n\n

      In case you have any of this package pre-installed and don't want to run entire script, see the script<\/a> and pick the packages you don't have currently installed. The script is fine-tuned for Discourse, and includes all the packages required for Discourse installation.<\/em><\/p>\n\n

      Now that we have installed Discourse dependencies, let's move on to install Discourse itself.<\/p>\n\n

      Clone Discourse<\/h2>\n\n

      Clone the Discourse repository in ~/discourse<\/code> folder:<\/p>\n\n

      git clone https://github.com/discourse/discourse.git ~/discourse<\/code><\/pre>\n\n

      <\/p>\n\n

      ~<\/code> indicates home folder, so Discourse source code will be available in your home folder.<\/p>\n\n

      Setup Database<\/h2>\n\n

      Open psql prompt:<\/p>\n\n

      psql postgres<\/code><\/pre>\n\n

      <\/p>\n\n

      Create discourse_development<\/strong> and discourse_test<\/strong> database with your account short name<\/a><\/em> specified as role:<\/p>\n\n

      CREATE DATABASE discourse_development WITH OWNER techapj ENCODING 'UTF8' TEMPLATE template0;\nCREATE DATABASE discourse_test WITH OWNER techapj ENCODING 'UTF8' TEMPLATE template0;<\/code><\/pre>\n\n

      Note that in above commands I specified the role as techapj<\/em>, this means that my short name<\/a> is techapj<\/em>, replace this with your own short name<\/a>.<\/strong><\/p>\n\n

      <\/p>\n\n

      Exit psql prompt by pressing control<\/kbd>d<\/kbd><\/p>\n\n

      Now access psql prompt in discourse_development<\/strong> database as your short name<\/em> user:<\/p>\n\n

      psql -d discourse_development -U techapj -h localhost<\/code><\/pre>\n\n

      Run following commands, separately:<\/p>\n\n

      CREATE EXTENSION pg_trgm;\nCREATE EXTENSION hstore;<\/code><\/pre>\n\n

      <\/p>\n\n

      Exit psql prompt by pressing control<\/kbd>d<\/kbd><\/p>\n\n

      Now access psql prompt in discourse_test<\/strong> database as your short name<\/em> user:<\/p>\n\n

      psql -d discourse_test -U techapj -h localhost<\/code><\/pre>\n\n

      Run following commands, separately:<\/p>\n\n

      CREATE EXTENSION pg_trgm;\nCREATE EXTENSION hstore;<\/code><\/pre>\n\n

      <\/p>\n\n

      Exit psql prompt by pressing control<\/kbd>d<\/kbd><\/p>\n\n

      You have set-up the database successfully!<\/p>\n\n

      Bootstrap Discourse<\/h2>\n\n

      Switch to your Discourse folder:<\/p>\n\n

      cd ~/discourse<\/code><\/pre>\n\n

      Install the needed gems<\/p>\n\n

      bundle install<\/code><\/pre>\n\n

      <\/p>\n\n

      Now that you have successfully installed gems, run this command:<\/p>\n\n

      bundle exec rake db:migrate db:test:prepare db:seed_fu<\/code><\/pre>\n\n

      Try running the specs: <\/p>\n\n

      bundle exec rake autospec<\/code><\/pre>\n\n

      <\/p>\n\n

      All the tests should pass.<\/p>\n\n

      Start rails server:<\/p>\n\n

      bundle exec rails server<\/code><\/pre>\n\n

      <\/p>\n\n

      You should now be able to connect with your Discourse app on http://localhost:3000<\/a> - try it out!<\/p>\n\n

      \nScreen Shot 2014-05-19 at 13.04.01.png<\/span>1255x461 98.7 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

      Create New Admin<\/h2>\n\n

      To create a new admin, run the following commands in rails console:<\/p>\n\n

      RAILS_ENV=development bundle exec rake admin:create<\/code><\/pre>\n\n

      Just enter your input as suggested, you can create an admin account. <\/p>\n\n

      \nfccdb29463e82f23.png<\/span>1919x430<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

      \nScreen Shot 2014-05-19 at 13.20.02.png<\/span>1256x756 124 KB<\/span><\/span>\n<\/div><\/a><\/div> <\/p>\n\n

      Happy hacking!<\/p>", -       "post_number": 1, -       "post_type": 1, -       "updated_at": "2015-04-26T06:51:23.549Z", -       "like_count": 13, -       "reply_count": 1, -       "reply_to_post_number": null, -       "quote_count": 0, -       "avg_time": 36, -       "incoming_link_count": 1483, -       "reads": 274, -       "score": 7985.4, -       "yours": false, -       "topic_id": 15772, -       "topic_slug": "beginners-guide-to-install-discourse-on-mac-os-x-for-development", -       "display_username": "Arpit Jalan", -       "primary_group_name": null, -       "version": 12, -       "can_edit": false, -       "can_delete": false, -       "can_recover": false, -       "user_title": "team", -       "actions_summary": [ -         { -           "id": 2, -           "count": 13, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 3, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 4, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 5, -           "count": 0, -           "hidden": true, -           "can_act": false -         }, -         { -           "id": 6, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 7, -           "count": 0, -           "hidden": false, -           "can_act": false -         }, -         { -           "id": 8, -           "count": 0, -           "hidden": false, -           "can_act": false -         } -       ], -       "moderator": true, -       "admin": true, -       "staff": true, -       "user_id": 8222, -       "hidden": false, -       "hidden_reason_id": null, -       "trust_level": 4, -       "deleted_at": null, -       "user_deleted": false, -       "edit_reason": "", -       "can_view_edit_history": true, -       "wiki": true, -       "blurb": "So you want to set up Discourse on Mac OS X to hack on and develop with? We'll assume that you don't have Ruby/Rails/Postgre/Redis installed on your Mac. Let's be..." -     }, -     { -       "id": 38398, -       "name": "Eric Carlson", -       "username": "ecuk", -       "avatar_template": "/letter_avatar/ecuk/{size}/5_fcf819f9b3791cb8c87edf29c8984f83.png", -       "uploaded_avatar_id": null, -       "created_at": "2014-01-24T15:08:06.111Z", -       "cooked": "

      Continuing the discussion from Log of setting up Docker in Virtualbox<\/a>:<\/p>\n\n

      `); +
      ` + ); } }); @@ -56,14 +54,14 @@ var generateClickEventOn = function(selector) { }; QUnit.test("does not track clicks on lightboxes", assert => { - var clickEvent = generateClickEventOn('.lightbox'); + var clickEvent = generateClickEventOn(".lightbox"); sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); assert.ok(!clickEvent.preventDefault.calledOnce); }); QUnit.test("it calls preventDefault when clicking on an a", assert => { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); sandbox.stub(clickEvent, "preventDefault"); track(clickEvent); assert.ok(clickEvent.preventDefault.calledOnce); @@ -71,37 +69,37 @@ QUnit.test("it calls preventDefault when clicking on an a", assert => { }); QUnit.test("does not track clicks when forcibly disabled", assert => { - assert.ok(track(generateClickEventOn('.no-track-link'))); + assert.ok(track(generateClickEventOn(".no-track-link"))); }); QUnit.test("does not track clicks on back buttons", assert => { - assert.ok(track(generateClickEventOn('.back'))); + assert.ok(track(generateClickEventOn(".back"))); }); QUnit.test("does not track clicks on category badges", assert => { - assert.ok(track(generateClickEventOn('.hashtag'))); + assert.ok(track(generateClickEventOn(".hashtag"))); }); QUnit.test("removes the href and put it as a data attribute", assert => { - track(generateClickEventOn('a')); + track(generateClickEventOn("a")); - var $link = fixture('a').first(); - assert.ok($link.hasClass('no-href')); - assert.equal($link.data('href'), 'http://www.google.com'); - assert.blank($link.attr('href')); - assert.ok($link.data('auto-route')); + var $link = fixture("a").first(); + assert.ok($link.hasClass("no-href")); + assert.equal($link.data("href"), "http://www.google.com"); + assert.blank($link.attr("href")); + assert.ok($link.data("auto-route")); assert.ok(DiscourseURL.redirectTo.calledOnce); }); asyncTestDiscourse("restores the href after a while", function(assert) { assert.expect(1); - track(generateClickEventOn('a')); + track(generateClickEventOn("a")); const done = assert.async(); setTimeout(function() { done(); - assert.equal(fixture('a').attr('href'), "http://www.google.com"); + assert.equal(fixture("a").attr("href"), "http://www.google.com"); }, 75); }); @@ -112,18 +110,28 @@ var trackRightClick = function(target) { }; QUnit.test("right clicks change the href", assert => { - assert.ok(trackRightClick('a')); - assert.equal(fixture('a').first().prop('href'), "http://www.google.com/"); + assert.ok(trackRightClick("a")); + assert.equal( + fixture("a") + .first() + .prop("href"), + "http://www.google.com/" + ); }); QUnit.test("right clicks are tracked", assert => { Discourse.SiteSettings.track_external_right_clicks = true; - trackRightClick('a'); - assert.equal(fixture('a').first().attr('href'), "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337"); + trackRightClick("a"); + assert.equal( + fixture("a") + .first() + .attr("href"), + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" + ); }); QUnit.test("preventDefault is not called for right clicks", assert => { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); clickEvent.which = 3; sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); @@ -132,7 +140,7 @@ QUnit.test("preventDefault is not called for right clicks", assert => { var testOpenInANewTab = function(description, clickEventModifier) { test(description, function(assert) { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); clickEventModifier(clickEvent); sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); @@ -140,19 +148,27 @@ var testOpenInANewTab = function(description, clickEventModifier) { }); }; -testOpenInANewTab("it opens in a new tab when pressing shift", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing shift", function( + clickEvent +) { clickEvent.shiftKey = true; }); -testOpenInANewTab("it opens in a new tab when pressing meta", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing meta", function( + clickEvent +) { clickEvent.metaKey = true; }); -testOpenInANewTab("it opens in a new tab when pressing ctrl", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing ctrl", function( + clickEvent +) { clickEvent.ctrlKey = true; }); -testOpenInANewTab("it opens in a new tab on middle click", function(clickEvent) { +testOpenInANewTab("it opens in a new tab on middle click", function( + clickEvent +) { clickEvent.button = 2; }); @@ -160,7 +176,7 @@ QUnit.test("tracks via AJAX if we're on the same site", assert => { sandbox.stub(DiscourseURL, "routeTo"); sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - assert.ok(!track(generateClickEventOn('#same-site'))); + assert.ok(!track(generateClickEventOn("#same-site"))); assert.ok(DiscourseURL.routeTo.calledOnce); }); @@ -168,19 +184,31 @@ QUnit.test("does not track via AJAX for attachments", assert => { sandbox.stub(DiscourseURL, "routeTo"); sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - assert.ok(!track(generateClickEventOn('.attachment'))); + assert.ok(!track(generateClickEventOn(".attachment"))); assert.ok(DiscourseURL.redirectTo.calledOnce); }); QUnit.test("tracks custom urls when opening in another window", assert => { - var clickEvent = generateClickEventOn('a'); - sandbox.stub(Discourse.User, "currentProp").withArgs('external_links_in_new_tab').returns(true); + var clickEvent = generateClickEventOn("a"); + sandbox + .stub(Discourse.User, "currentProp") + .withArgs("external_links_in_new_tab") + .returns(true); assert.ok(!track(clickEvent)); - assert.ok(windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337', '_blank')); + assert.ok( + windowOpen.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337", + "_blank" + ) + ); }); QUnit.test("tracks custom urls when opening in another window", assert => { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); assert.ok(!track(clickEvent)); - assert.ok(redirectTo.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337')); + assert.ok( + redirectTo.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" + ) + ); }); diff --git a/test/javascripts/lib/click-track-profile-page-test.js.es6 b/test/javascripts/lib/click-track-profile-page-test.js.es6 index c809059c06..fdf3162f13 100644 --- a/test/javascripts/lib/click-track-profile-page-test.js.es6 +++ b/test/javascripts/lib/click-track-profile-page-test.js.es6 @@ -1,15 +1,12 @@ import DiscourseURL from "discourse/lib/url"; import ClickTrack from "discourse/lib/click-track"; -var windowOpen, - win, - redirectTo; +var windowOpen, win, redirectTo; QUnit.module("lib:click-track-profile-page", { beforeEach() { - // Prevent any of these tests from navigating away - win = {focus: function() { } }; + win = { focus: function() {} }; redirectTo = sandbox.stub(DiscourseURL, "redirectTo"); windowOpen = sandbox.stub(window, "open").returns(win); sandbox.stub(win, "focus"); @@ -38,7 +35,8 @@ QUnit.module("lib:click-track-profile-page", { forum log.txt #hashtag -

      `); +

      ` + ); } }); @@ -50,14 +48,14 @@ var generateClickEventOn = function(selector) { }; QUnit.test("does not track clicks on lightboxes", assert => { - var clickEvent = generateClickEventOn('.lightbox'); + var clickEvent = generateClickEventOn(".lightbox"); sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); assert.ok(!clickEvent.preventDefault.calledOnce); }); QUnit.test("it calls preventDefault when clicking on an a", assert => { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); sandbox.stub(clickEvent, "preventDefault"); track(clickEvent); assert.ok(clickEvent.preventDefault.calledOnce); @@ -65,37 +63,37 @@ QUnit.test("it calls preventDefault when clicking on an a", assert => { }); QUnit.test("does not track clicks when forcibly disabled", assert => { - assert.ok(track(generateClickEventOn('.no-track-link'))); + assert.ok(track(generateClickEventOn(".no-track-link"))); }); QUnit.test("does not track clicks on back buttons", assert => { - assert.ok(track(generateClickEventOn('.back'))); + assert.ok(track(generateClickEventOn(".back"))); }); QUnit.test("does not track clicks on category badges", assert => { - assert.ok(track(generateClickEventOn('.hashtag'))); + assert.ok(track(generateClickEventOn(".hashtag"))); }); QUnit.test("removes the href and put it as a data attribute", assert => { - track(generateClickEventOn('a')); + track(generateClickEventOn("a")); - var $link = fixture('a').first(); - assert.ok($link.hasClass('no-href')); - assert.equal($link.data('href'), 'http://www.google.com'); - assert.blank($link.attr('href')); - assert.ok($link.data('auto-route')); + var $link = fixture("a").first(); + assert.ok($link.hasClass("no-href")); + assert.equal($link.data("href"), "http://www.google.com"); + assert.blank($link.attr("href")); + assert.ok($link.data("auto-route")); assert.ok(DiscourseURL.redirectTo.calledOnce); }); asyncTestDiscourse("restores the href after a while", function(assert) { assert.expect(1); - track(generateClickEventOn('a')); + track(generateClickEventOn("a")); const done = assert.async(); setTimeout(function() { done(); - assert.equal(fixture('a').attr('href'), "http://www.google.com"); + assert.equal(fixture("a").attr("href"), "http://www.google.com"); }, 75); }); @@ -106,24 +104,39 @@ var trackRightClick = function(target) { }; QUnit.test("right clicks change the href", assert => { - assert.ok(trackRightClick('a')); - assert.equal(fixture('a').first().prop('href'), "http://www.google.com/"); + assert.ok(trackRightClick("a")); + assert.equal( + fixture("a") + .first() + .prop("href"), + "http://www.google.com/" + ); }); QUnit.test("right clicks are tracked", assert => { Discourse.SiteSettings.track_external_right_clicks = true; - trackRightClick('a'); - assert.equal(fixture('.first a').first().attr('href'), "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337"); + trackRightClick("a"); + assert.equal( + fixture(".first a") + .first() + .attr("href"), + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" + ); }); QUnit.test("right clicks are tracked for second excerpt", assert => { Discourse.SiteSettings.track_external_right_clicks = true; - trackRightClick('.second a'); - assert.equal(fixture('.second a').first().attr('href'), "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331"); + trackRightClick(".second a"); + assert.equal( + fixture(".second a") + .first() + .attr("href"), + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331" + ); }); QUnit.test("preventDefault is not called for right clicks", assert => { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); clickEvent.which = 3; sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); @@ -132,7 +145,7 @@ QUnit.test("preventDefault is not called for right clicks", assert => { var testOpenInANewTab = function(description, clickEventModifier) { test(description, function(assert) { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); clickEventModifier(clickEvent); sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); @@ -140,19 +153,27 @@ var testOpenInANewTab = function(description, clickEventModifier) { }); }; -testOpenInANewTab("it opens in a new tab when pressing shift", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing shift", function( + clickEvent +) { clickEvent.shiftKey = true; }); -testOpenInANewTab("it opens in a new tab when pressing meta", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing meta", function( + clickEvent +) { clickEvent.metaKey = true; }); -testOpenInANewTab("it opens in a new tab when pressing ctrl", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing ctrl", function( + clickEvent +) { clickEvent.ctrlKey = true; }); -testOpenInANewTab("it opens in a new tab on middle click", function(clickEvent) { +testOpenInANewTab("it opens in a new tab on middle click", function( + clickEvent +) { clickEvent.button = 2; }); @@ -160,7 +181,7 @@ QUnit.test("tracks via AJAX if we're on the same site", assert => { sandbox.stub(DiscourseURL, "routeTo"); sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - assert.ok(!track(generateClickEventOn('#same-site'))); + assert.ok(!track(generateClickEventOn("#same-site"))); assert.ok(DiscourseURL.routeTo.calledOnce); }); @@ -168,32 +189,62 @@ QUnit.test("does not track via AJAX for attachments", assert => { sandbox.stub(DiscourseURL, "routeTo"); sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - assert.ok(!track(generateClickEventOn('.attachment'))); + assert.ok(!track(generateClickEventOn(".attachment"))); assert.ok(DiscourseURL.redirectTo.calledOnce); }); QUnit.test("tracks custom urls when opening in another window", assert => { - var clickEvent = generateClickEventOn('a'); - sandbox.stub(Discourse.User, "currentProp").withArgs('external_links_in_new_tab').returns(true); + var clickEvent = generateClickEventOn("a"); + sandbox + .stub(Discourse.User, "currentProp") + .withArgs("external_links_in_new_tab") + .returns(true); assert.ok(!track(clickEvent)); - assert.ok(windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337', '_blank')); + assert.ok( + windowOpen.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337", + "_blank" + ) + ); }); -QUnit.test("tracks custom urls on second excerpt when opening in another window", assert => { - var clickEvent = generateClickEventOn('.second a'); - sandbox.stub(Discourse.User, "currentProp").withArgs('external_links_in_new_tab').returns(true); - assert.ok(!track(clickEvent)); - assert.ok(windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331', '_blank')); -}); +QUnit.test( + "tracks custom urls on second excerpt when opening in another window", + assert => { + var clickEvent = generateClickEventOn(".second a"); + sandbox + .stub(Discourse.User, "currentProp") + .withArgs("external_links_in_new_tab") + .returns(true); + assert.ok(!track(clickEvent)); + assert.ok( + windowOpen.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331", + "_blank" + ) + ); + } +); QUnit.test("tracks custom urls when opening in another window", assert => { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); assert.ok(!track(clickEvent)); - assert.ok(redirectTo.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337')); + assert.ok( + redirectTo.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" + ) + ); }); -QUnit.test("tracks custom urls on second excerpt when opening in another window", assert => { - var clickEvent = generateClickEventOn('.second a'); - assert.ok(!track(clickEvent)); - assert.ok(redirectTo.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331')); -}); +QUnit.test( + "tracks custom urls on second excerpt when opening in another window", + assert => { + var clickEvent = generateClickEventOn(".second a"); + assert.ok(!track(clickEvent)); + assert.ok( + redirectTo.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=24&topic_id=7331" + ) + ); + } +); diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index dc6013ee08..3933768c15 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -1,14 +1,12 @@ import DiscourseURL from "discourse/lib/url"; import ClickTrack from "discourse/lib/click-track"; -var windowOpen, - win, - redirectTo; +var windowOpen, win, redirectTo; QUnit.module("lib:click-track", { beforeEach() { // Prevent any of these tests from navigating away - win = {focus: function() { } }; + win = { focus: function() {} }; redirectTo = sandbox.stub(DiscourseURL, "redirectTo"); windowOpen = sandbox.stub(window, "open").returns(win); sandbox.stub(win, "focus"); @@ -35,7 +33,8 @@ QUnit.module("lib:click-track", { foobar -
      `); +
      ` + ); } }); @@ -47,14 +46,14 @@ var generateClickEventOn = function(selector) { }; QUnit.test("does not track clicks on lightboxes", function(assert) { - var clickEvent = generateClickEventOn('.lightbox'); + var clickEvent = generateClickEventOn(".lightbox"); sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); assert.ok(!clickEvent.preventDefault.calledOnce); }); QUnit.test("it calls preventDefault when clicking on an a", function(assert) { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); sandbox.stub(clickEvent, "preventDefault"); track(clickEvent); assert.ok(clickEvent.preventDefault.calledOnce); @@ -62,90 +61,106 @@ QUnit.test("it calls preventDefault when clicking on an a", function(assert) { }); QUnit.test("does not track clicks when forcibly disabled", function(assert) { - assert.ok(track(generateClickEventOn('.no-track-link'))); + assert.ok(track(generateClickEventOn(".no-track-link"))); }); QUnit.test("does not track clicks on back buttons", function(assert) { - assert.ok(track(generateClickEventOn('.back'))); + assert.ok(track(generateClickEventOn(".back"))); }); QUnit.test("does not track clicks in quotes", function(assert) { - track(generateClickEventOn('.inside-quote')); + track(generateClickEventOn(".inside-quote")); assert.ok(DiscourseURL.redirectTo.calledWith("http://discuss.domain.com")); }); QUnit.test("does not track clicks on category badges", assert => { - assert.ok(track(generateClickEventOn('.hashtag'))); + assert.ok(track(generateClickEventOn(".hashtag"))); }); QUnit.test("does not track clicks on mailto", function(assert) { - assert.ok(track(generateClickEventOn('.mailto'))); + assert.ok(track(generateClickEventOn(".mailto"))); }); QUnit.test("removes the href and put it as a data attribute", function(assert) { - track(generateClickEventOn('a')); + track(generateClickEventOn("a")); - var $link = fixture('a').first(); - assert.ok($link.hasClass('no-href')); - assert.equal($link.data('href'), 'http://www.google.com'); - assert.blank($link.attr('href')); - assert.ok($link.data('auto-route')); + var $link = fixture("a").first(); + assert.ok($link.hasClass("no-href")); + assert.equal($link.data("href"), "http://www.google.com"); + assert.blank($link.attr("href")); + assert.ok($link.data("auto-route")); assert.ok(DiscourseURL.redirectTo.calledOnce); }); asyncTestDiscourse("restores the href after a while", function(assert) { assert.expect(1); - track(generateClickEventOn('a')); + track(generateClickEventOn("a")); const done = assert.async(); setTimeout(function() { done(); - assert.equal(fixture('a').attr('href'), "http://www.google.com"); + assert.equal(fixture("a").attr("href"), "http://www.google.com"); }, 75); }); var badgeClickCount = function(assert, id, expected) { - track(generateClickEventOn('#' + id)); - var $badge = $('span.badge', fixture('#' + id).first()); + track(generateClickEventOn("#" + id)); + var $badge = $("span.badge", fixture("#" + id).first()); assert.equal(parseInt($badge.html(), 10), expected); }; QUnit.test("does not update badge clicks on my own link", function(assert) { - sandbox.stub(Discourse.User, 'currentProp').withArgs('id').returns(314); - badgeClickCount(assert, 'with-badge', 1); + sandbox + .stub(Discourse.User, "currentProp") + .withArgs("id") + .returns(314); + badgeClickCount(assert, "with-badge", 1); }); QUnit.test("does not update badge clicks in my own post", function(assert) { - sandbox.stub(Discourse.User, 'currentProp').withArgs('id').returns(3141); - badgeClickCount(assert, 'with-badge-but-not-mine', 1); + sandbox + .stub(Discourse.User, "currentProp") + .withArgs("id") + .returns(3141); + badgeClickCount(assert, "with-badge-but-not-mine", 1); }); QUnit.test("updates badge counts correctly", function(assert) { - badgeClickCount(assert, 'inside-onebox', 1); - badgeClickCount(assert, 'inside-onebox-forced', 2); - badgeClickCount(assert, 'with-badge', 2); + badgeClickCount(assert, "inside-onebox", 1); + badgeClickCount(assert, "inside-onebox-forced", 2); + badgeClickCount(assert, "with-badge", 2); }); var trackRightClick = function() { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); clickEvent.which = 3; return track(clickEvent); }; QUnit.test("right clicks change the href", function(assert) { assert.ok(trackRightClick()); - assert.equal(fixture('a').first().prop('href'), "http://www.google.com/"); + assert.equal( + fixture("a") + .first() + .prop("href"), + "http://www.google.com/" + ); }); QUnit.test("right clicks are tracked", function(assert) { Discourse.SiteSettings.track_external_right_clicks = true; trackRightClick(); - assert.equal(fixture('a').first().attr('href'), "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337"); + assert.equal( + fixture("a") + .first() + .attr("href"), + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" + ); }); QUnit.test("preventDefault is not called for right clicks", function(assert) { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); clickEvent.which = 3; sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); @@ -154,7 +169,7 @@ QUnit.test("preventDefault is not called for right clicks", function(assert) { var testOpenInANewTab = function(description, clickEventModifier) { test(description, function(assert) { - var clickEvent = generateClickEventOn('a'); + var clickEvent = generateClickEventOn("a"); clickEventModifier(clickEvent); sandbox.stub(clickEvent, "preventDefault"); assert.ok(track(clickEvent)); @@ -162,19 +177,27 @@ var testOpenInANewTab = function(description, clickEventModifier) { }); }; -testOpenInANewTab("it opens in a new tab when pressing shift", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing shift", function( + clickEvent +) { clickEvent.shiftKey = true; }); -testOpenInANewTab("it opens in a new tab when pressing meta", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing meta", function( + clickEvent +) { clickEvent.metaKey = true; }); -testOpenInANewTab("it opens in a new tab when pressing ctrl", function(clickEvent) { +testOpenInANewTab("it opens in a new tab when pressing ctrl", function( + clickEvent +) { clickEvent.ctrlKey = true; }); -testOpenInANewTab("it opens in a new tab on middle click", function(clickEvent) { +testOpenInANewTab("it opens in a new tab on middle click", function( + clickEvent +) { clickEvent.button = 2; }); @@ -182,7 +205,7 @@ QUnit.test("tracks via AJAX if we're on the same site", function(assert) { sandbox.stub(DiscourseURL, "routeTo"); sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - assert.ok(!track(generateClickEventOn('#same-site'))); + assert.ok(!track(generateClickEventOn("#same-site"))); assert.ok(DiscourseURL.routeTo.calledOnce); }); @@ -190,19 +213,35 @@ QUnit.test("does not track via AJAX for attachments", function(assert) { sandbox.stub(DiscourseURL, "routeTo"); sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); - assert.ok(!track(generateClickEventOn('.attachment'))); + assert.ok(!track(generateClickEventOn(".attachment"))); assert.ok(DiscourseURL.redirectTo.calledOnce); }); -QUnit.test("tracks custom urls when opening in another window", function(assert) { - var clickEvent = generateClickEventOn('a'); - sandbox.stub(Discourse.User, "currentProp").withArgs('external_links_in_new_tab').returns(true); +QUnit.test("tracks custom urls when opening in another window", function( + assert +) { + var clickEvent = generateClickEventOn("a"); + sandbox + .stub(Discourse.User, "currentProp") + .withArgs("external_links_in_new_tab") + .returns(true); assert.ok(!track(clickEvent)); - assert.ok(windowOpen.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337', '_blank')); + assert.ok( + windowOpen.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337", + "_blank" + ) + ); }); -QUnit.test("tracks custom urls when opening in another window", function(assert) { - var clickEvent = generateClickEventOn('a'); +QUnit.test("tracks custom urls when opening in another window", function( + assert +) { + var clickEvent = generateClickEventOn("a"); assert.ok(!track(clickEvent)); - assert.ok(redirectTo.calledWith('/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337')); + assert.ok( + redirectTo.calledWith( + "/clicks/track?url=http%3A%2F%2Fwww.google.com&post_id=42&topic_id=1337" + ) + ); }); diff --git a/test/javascripts/lib/computed-test.js.es6 b/test/javascripts/lib/computed-test.js.es6 index 6752784a2c..26158686ad 100644 --- a/test/javascripts/lib/computed-test.js.es6 +++ b/test/javascripts/lib/computed-test.js.es6 @@ -1,4 +1,11 @@ -import { setting, propertyEqual, propertyNotEqual, fmt, i18n, url } from 'discourse/lib/computed'; +import { + setting, + propertyEqual, + propertyNotEqual, + fmt, + i18n, + url +} from "discourse/lib/computed"; QUnit.module("lib:computed", { beforeEach() { @@ -14,91 +21,135 @@ QUnit.module("lib:computed", { QUnit.test("setting", assert => { var t = Em.Object.extend({ - vehicle: setting('vehicle'), - missingProp: setting('madeUpThing') + vehicle: setting("vehicle"), + missingProp: setting("madeUpThing") }).create(); Discourse.SiteSettings.vehicle = "airplane"; - assert.equal(t.get('vehicle'), "airplane", "it has the value of the site setting"); - assert.ok(!t.get('missingProp'), "it is falsy when the site setting is not defined"); + assert.equal( + t.get("vehicle"), + "airplane", + "it has the value of the site setting" + ); + assert.ok( + !t.get("missingProp"), + "it is falsy when the site setting is not defined" + ); }); QUnit.test("propertyEqual", assert => { var t = Em.Object.extend({ - same: propertyEqual('cookies', 'biscuits') + same: propertyEqual("cookies", "biscuits") }).create({ cookies: 10, biscuits: 10 }); - assert.ok(t.get('same'), "it is true when the properties are the same"); - t.set('biscuits', 9); - assert.ok(!t.get('same'), "it isn't true when one property is different"); + assert.ok(t.get("same"), "it is true when the properties are the same"); + t.set("biscuits", 9); + assert.ok(!t.get("same"), "it isn't true when one property is different"); }); QUnit.test("propertyNotEqual", assert => { var t = Em.Object.extend({ - diff: propertyNotEqual('cookies', 'biscuits') + diff: propertyNotEqual("cookies", "biscuits") }).create({ cookies: 10, biscuits: 10 }); - assert.ok(!t.get('diff'), "it isn't true when the properties are the same"); - t.set('biscuits', 9); - assert.ok(t.get('diff'), "it is true when one property is different"); + assert.ok(!t.get("diff"), "it isn't true when the properties are the same"); + t.set("biscuits", 9); + assert.ok(t.get("diff"), "it is true when one property is different"); }); - QUnit.test("fmt", assert => { var t = Em.Object.extend({ - exclaimyUsername: fmt('username', "!!! %@ !!!"), - multiple: fmt('username', 'mood', "%@ is %@") + exclaimyUsername: fmt("username", "!!! %@ !!!"), + multiple: fmt("username", "mood", "%@ is %@") }).create({ - username: 'eviltrout', + username: "eviltrout", mood: "happy" }); - assert.equal(t.get('exclaimyUsername'), '!!! eviltrout !!!', "it inserts the string"); - assert.equal(t.get('multiple'), "eviltrout is happy", "it inserts multiple strings"); + assert.equal( + t.get("exclaimyUsername"), + "!!! eviltrout !!!", + "it inserts the string" + ); + assert.equal( + t.get("multiple"), + "eviltrout is happy", + "it inserts multiple strings" + ); - t.set('username', 'codinghorror'); - assert.equal(t.get('multiple'), "codinghorror is happy", "it supports changing properties"); - t.set('mood', 'ecstatic'); - assert.equal(t.get('multiple'), "codinghorror is ecstatic", "it supports changing another property"); + t.set("username", "codinghorror"); + assert.equal( + t.get("multiple"), + "codinghorror is happy", + "it supports changing properties" + ); + t.set("mood", "ecstatic"); + assert.equal( + t.get("multiple"), + "codinghorror is ecstatic", + "it supports changing another property" + ); }); - QUnit.test("i18n", assert => { var t = Em.Object.extend({ - exclaimyUsername: i18n('username', "!!! %@ !!!"), - multiple: i18n('username', 'mood', "%@ is %@") + exclaimyUsername: i18n("username", "!!! %@ !!!"), + multiple: i18n("username", "mood", "%@ is %@") }).create({ - username: 'eviltrout', + username: "eviltrout", mood: "happy" }); - assert.equal(t.get('exclaimyUsername'), '%@ translated: !!! eviltrout !!!', "it inserts the string and then translates"); - assert.equal(t.get('multiple'), "%@ translated: eviltrout is happy", "it inserts multiple strings and then translates"); + assert.equal( + t.get("exclaimyUsername"), + "%@ translated: !!! eviltrout !!!", + "it inserts the string and then translates" + ); + assert.equal( + t.get("multiple"), + "%@ translated: eviltrout is happy", + "it inserts multiple strings and then translates" + ); - t.set('username', 'codinghorror'); - assert.equal(t.get('multiple'), "%@ translated: codinghorror is happy", "it supports changing properties"); - t.set('mood', 'ecstatic'); - assert.equal(t.get('multiple'), "%@ translated: codinghorror is ecstatic", "it supports changing another property"); + t.set("username", "codinghorror"); + assert.equal( + t.get("multiple"), + "%@ translated: codinghorror is happy", + "it supports changing properties" + ); + t.set("mood", "ecstatic"); + assert.equal( + t.get("multiple"), + "%@ translated: codinghorror is ecstatic", + "it supports changing another property" + ); }); - QUnit.test("url", assert => { var t, testClass; testClass = Em.Object.extend({ - userUrl: url('username', "/u/%@") + userUrl: url("username", "/u/%@") }); - t = testClass.create({ username: 'eviltrout' }); - assert.equal(t.get('userUrl'), "/u/eviltrout", "it supports urls without a prefix"); + t = testClass.create({ username: "eviltrout" }); + assert.equal( + t.get("userUrl"), + "/u/eviltrout", + "it supports urls without a prefix" + ); Discourse.BaseUri = "/prefixed"; - t = testClass.create({ username: 'eviltrout' }); - assert.equal(t.get('userUrl'), "/prefixed/u/eviltrout", "it supports urls with a prefix"); + t = testClass.create({ username: "eviltrout" }); + assert.equal( + t.get("userUrl"), + "/prefixed/u/eviltrout", + "it supports urls with a prefix" + ); }); diff --git a/test/javascripts/lib/discourse-test.js.es6 b/test/javascripts/lib/discourse-test.js.es6 index 4bafe429ab..9627eafdbb 100644 --- a/test/javascripts/lib/discourse-test.js.es6 +++ b/test/javascripts/lib/discourse-test.js.es6 @@ -3,5 +3,9 @@ QUnit.module("lib:discourse"); QUnit.test("getURL on subfolder install", assert => { Discourse.BaseUri = "/forum"; assert.equal(Discourse.getURL("/"), "/forum/", "root url has subfolder"); - assert.equal(Discourse.getURL("/u/neil"), "/forum/u/neil", "relative url has subfolder"); -}); \ No newline at end of file + assert.equal( + Discourse.getURL("/u/neil"), + "/forum/u/neil", + "relative url has subfolder" + ); +}); diff --git a/test/javascripts/lib/emoji-test.js.es6 b/test/javascripts/lib/emoji-test.js.es6 index 8512b9f05c..47665a8c8a 100644 --- a/test/javascripts/lib/emoji-test.js.es6 +++ b/test/javascripts/lib/emoji-test.js.es6 @@ -1,27 +1,68 @@ -import { emojiSearch, IMAGE_VERSION as v } from 'pretty-text/emoji'; -import { emojiUnescape } from 'discourse/lib/text'; +import { emojiSearch, IMAGE_VERSION as v } from "pretty-text/emoji"; +import { emojiUnescape } from "discourse/lib/text"; -QUnit.module('lib:emoji'); +QUnit.module("lib:emoji"); QUnit.test("emojiUnescape", assert => { const testUnescape = (input, expected, description) => { assert.equal(emojiUnescape(input), expected, description); }; - testUnescape("Not emoji :O) :frog) :smile)", "Not emoji :O) :frog) :smile)", "title without emoji"); - testUnescape("Not emoji :frog :smile", "Not emoji :frog :smile", "end colon is not optional"); - testUnescape("emoticons :)", `emoticons slight_smile`, "emoticons are still supported"); - testUnescape("With emoji :O: :frog: :smile:", + testUnescape( + "Not emoji :O) :frog) :smile)", + "Not emoji :O) :frog) :smile)", + "title without emoji" + ); + testUnescape( + "Not emoji :frog :smile", + "Not emoji :frog :smile", + "end colon is not optional" + ); + testUnescape( + "emoticons :)", + `emoticons slight_smile`, + "emoticons are still supported" + ); + testUnescape( + "With emoji :O: :frog: :smile:", `With emoji O frog smile`, - "title with emoji"); - testUnescape("a:smile:a", "a:smile:a", "word characters not allowed next to emoji"); - testUnescape("(:frog:) :)", `(frog) slight_smile`, "non-word characters allowed next to emoji"); - testUnescape(":smile: hi", `smile hi`, "start of line"); - testUnescape("hi :smile:", `hi smile`, "end of line"); - testUnescape("hi :blonde_woman:t4:", `hi blonde_woman:t4`, "support for skin tones"); - testUnescape("hi :blonde_woman:t4: :blonde_man:t6:", `hi blonde_woman:t4 blonde_man:t6`, "support for multiple skin tones"); - testUnescape("hi :blonde_man:t6", "hi :blonde_man:t6", "end colon not optional for skin tones"); - + "title with emoji" + ); + testUnescape( + "a:smile:a", + "a:smile:a", + "word characters not allowed next to emoji" + ); + testUnescape( + "(:frog:) :)", + `(frog) slight_smile`, + "non-word characters allowed next to emoji" + ); + testUnescape( + ":smile: hi", + `smile hi`, + "start of line" + ); + testUnescape( + "hi :smile:", + `hi smile`, + "end of line" + ); + testUnescape( + "hi :blonde_woman:t4:", + `hi blonde_woman:t4`, + "support for skin tones" + ); + testUnescape( + "hi :blonde_woman:t4: :blonde_man:t6:", + `hi blonde_woman:t4 blonde_man:t6`, + "support for multiple skin tones" + ); + testUnescape( + "hi :blonde_man:t6", + "hi :blonde_man:t6", + "end colon not optional for skin tones" + ); }); QUnit.test("Emoji search", assert => { @@ -29,6 +70,5 @@ QUnit.test("Emoji search", assert => { assert.equal(emojiSearch("+1").length, 1); // able to find middle of line search - assert.equal(emojiSearch("check", {maxResults: 3}).length, 3); - + assert.equal(emojiSearch("check", { maxResults: 3 }).length, 3); }); diff --git a/test/javascripts/lib/formatter-test.js.es6 b/test/javascripts/lib/formatter-test.js.es6 index e7299cb464..693fdeb823 100644 --- a/test/javascripts/lib/formatter-test.js.es6 +++ b/test/javascripts/lib/formatter-test.js.es6 @@ -1,10 +1,18 @@ var clock; -import { relativeAge, autoUpdatingRelativeAge, updateRelativeAge, breakUp, number, longDate, durationTiny } from 'discourse/lib/formatter'; +import { + relativeAge, + autoUpdatingRelativeAge, + updateRelativeAge, + breakUp, + number, + longDate, + durationTiny +} from "discourse/lib/formatter"; QUnit.module("lib:formatter", { beforeEach() { - clock = sinon.useFakeTimers(new Date(2012,11,31,12,0).getTime()); + clock = sinon.useFakeTimers(new Date(2012, 11, 31, 12, 0).getTime()); }, afterEach() { @@ -14,12 +22,12 @@ QUnit.module("lib:formatter", { var format = "tiny"; var leaveAgo = false; -var mins_ago = function(mins){ - return new Date((new Date()) - mins * 60 * 1000); +var mins_ago = function(mins) { + return new Date(new Date() - mins * 60 * 1000); }; var formatMins = function(mins) { - return relativeAge(mins_ago(mins), {format: format, leaveAgo: leaveAgo}); + return relativeAge(mins_ago(mins), { format: format, leaveAgo: leaveAgo }); }; var formatHours = function(hours) { @@ -30,19 +38,22 @@ var formatDays = function(days) { return formatHours(days * 24); }; -var shortDate = function(days){ - return moment().subtract(days, 'days').format('MMM D'); +var shortDate = function(days) { + return moment() + .subtract(days, "days") + .format("MMM D"); }; QUnit.test("formating medium length dates", assert => { - format = "medium"; - var strip = function(html){ + var strip = function(html) { return $(html).text(); }; - var shortDateYear = function(days){ - return moment().subtract(days, 'days').format("MMM D, 'YY"); + var shortDateYear = function(days) { + return moment() + .subtract(days, "days") + .format("MMM D, 'YY"); }; leaveAgo = true; @@ -76,15 +87,17 @@ QUnit.test("formating medium length dates", assert => { assert.equal($(formatDays(0)).attr("class"), "date"); clock.restore(); - clock = sinon.useFakeTimers(new Date(2012,0,9,12,0).getTime()); // Jan 9, 2012 + clock = sinon.useFakeTimers(new Date(2012, 0, 9, 12, 0).getTime()); // Jan 9, 2012 assert.equal(strip(formatDays(8)), shortDate(8)); assert.equal(strip(formatDays(10)), shortDateYear(10)); }); QUnit.test("formating tiny dates", assert => { - var shortDateYear = function(days){ - return moment().subtract(days, 'days').format("MMM 'YY"); + var shortDateYear = function(days) { + return moment() + .subtract(days, "days") + .format("MMM 'YY"); }; format = "tiny"; @@ -103,7 +116,7 @@ QUnit.test("formating tiny dates", assert => { assert.equal(formatDays(365), shortDate(365)); assert.equal(formatDays(366), shortDateYear(366)); // leap year assert.equal(formatDays(500), shortDateYear(500)); - assert.equal(formatDays(365*2 + 1), shortDateYear(365*2 + 1)); // one leap year + assert.equal(formatDays(365 * 2 + 1), shortDateYear(365 * 2 + 1)); // one leap year var originalValue = Discourse.SiteSettings.relative_date_duration; Discourse.SiteSettings.relative_date_duration = 7; @@ -124,14 +137,14 @@ QUnit.test("formating tiny dates", assert => { assert.equal(formatDays(366), shortDateYear(366)); Discourse.SiteSettings.relative_date_duration = null; - assert.equal(formatDays(1), '1d'); - assert.equal(formatDays(14), '14d'); + assert.equal(formatDays(1), "1d"); + assert.equal(formatDays(14), "14d"); assert.equal(formatDays(15), shortDate(15)); Discourse.SiteSettings.relative_date_duration = 14; clock.restore(); - clock = sinon.useFakeTimers(new Date(2012,0,12,12,0).getTime()); // Jan 12, 2012 + clock = sinon.useFakeTimers(new Date(2012, 0, 12, 12, 0).getTime()); // Jan 12, 2012 assert.equal(formatDays(11), "11d"); assert.equal(formatDays(14), "14d"); @@ -139,7 +152,7 @@ QUnit.test("formating tiny dates", assert => { assert.equal(formatDays(366), shortDateYear(366)); clock.restore(); - clock = sinon.useFakeTimers(new Date(2012,0,20,12,0).getTime()); // Jan 20, 2012 + clock = sinon.useFakeTimers(new Date(2012, 0, 20, 12, 0).getTime()); // Jan 20, 2012 assert.equal(formatDays(14), "14d"); assert.equal(formatDays(15), shortDate(15)); @@ -149,51 +162,59 @@ QUnit.test("formating tiny dates", assert => { }); QUnit.test("autoUpdatingRelativeAge", assert => { - var d = moment().subtract(1, 'day').toDate(); + var d = moment() + .subtract(1, "day") + .toDate(); var $elem = $(autoUpdatingRelativeAge(d)); - assert.equal($elem.data('format'), "tiny"); - assert.equal($elem.data('time'), d.getTime()); - assert.equal($elem.attr('title'), undefined); + assert.equal($elem.data("format"), "tiny"); + assert.equal($elem.data("time"), d.getTime()); + assert.equal($elem.attr("title"), undefined); - $elem = $(autoUpdatingRelativeAge(d, {title: true})); - assert.equal($elem.attr('title'), longDate(d)); + $elem = $(autoUpdatingRelativeAge(d, { title: true })); + assert.equal($elem.attr("title"), longDate(d)); - $elem = $(autoUpdatingRelativeAge(d,{format: 'medium', title: true, leaveAgo: true})); - assert.equal($elem.data('format'), "medium-with-ago"); - assert.equal($elem.data('time'), d.getTime()); - assert.equal($elem.attr('title'), longDate(d)); - assert.equal($elem.html(), '1 day ago'); + $elem = $( + autoUpdatingRelativeAge(d, { + format: "medium", + title: true, + leaveAgo: true + }) + ); + assert.equal($elem.data("format"), "medium-with-ago"); + assert.equal($elem.data("time"), d.getTime()); + assert.equal($elem.attr("title"), longDate(d)); + assert.equal($elem.html(), "1 day ago"); - $elem = $(autoUpdatingRelativeAge(d,{format: 'medium'})); - assert.equal($elem.data('format'), "medium"); - assert.equal($elem.data('time'), d.getTime()); - assert.equal($elem.attr('title'), undefined); - assert.equal($elem.html(), '1 day'); + $elem = $(autoUpdatingRelativeAge(d, { format: "medium" })); + assert.equal($elem.data("format"), "medium"); + assert.equal($elem.data("time"), d.getTime()); + assert.equal($elem.attr("title"), undefined); + assert.equal($elem.html(), "1 day"); }); -QUnit.test("updateRelativeAge", assert =>{ - +QUnit.test("updateRelativeAge", assert => { var d = new Date(); var $elem = $(autoUpdatingRelativeAge(d)); - $elem.data('time', d.getTime() - 2 * 60 * 1000); + $elem.data("time", d.getTime() - 2 * 60 * 1000); updateRelativeAge($elem); assert.equal($elem.html(), "2m"); d = new Date(); - $elem = $(autoUpdatingRelativeAge(d, {format: 'medium', leaveAgo: true})); - $elem.data('time', d.getTime() - 2 * 60 * 1000); + $elem = $(autoUpdatingRelativeAge(d, { format: "medium", leaveAgo: true })); + $elem.data("time", d.getTime() - 2 * 60 * 1000); updateRelativeAge($elem); assert.equal($elem.html(), "2 mins ago"); }); -QUnit.test("breakUp", assert =>{ - - var b = function(s,hint){ return breakUp(s,hint); }; +QUnit.test("breakUp", assert => { + var b = function(s, hint) { + return breakUp(s, hint); + }; assert.equal(b("hello"), "hello"); assert.equal(b("helloworld"), "helloworld"); @@ -201,8 +222,10 @@ QUnit.test("breakUp", assert =>{ assert.equal(b("he_man"), "he_​man"); assert.equal(b("he11111"), "he​11111"); assert.equal(b("HRCBob"), "HRC​Bob"); - assert.equal(b("bobmarleytoo","Bob Marley Too"), "bob​marley​too"); - + assert.equal( + b("bobmarleytoo", "Bob Marley Too"), + "bob​marley​too" + ); }); QUnit.test("number", assert => { @@ -211,32 +234,59 @@ QUnit.test("number", assert => { assert.equal(number(NaN), "0", "it returns 0 for NaN"); assert.equal(number(3333), "3.3k", "it abbreviates thousands"); assert.equal(number(2499999), "2.5M", "it abbreviates millions"); + assert.equal(number("2499999.5"), "2.5M", "it abbreviates millions"); assert.equal(number(1000000), "1.0M", "it abbreviates a million"); assert.equal(number(999999), "999k", "it abbreviates hundreds of thousands"); - assert.equal(number(18.2), "18", "it returns a float number converted to an integer as a string"); - assert.equal(number(18.6, { ceil: true }), "19", "it ceils the value if requested"); + assert.equal( + number(18.2), + "18", + "it returns a float number rounded to an integer as a string" + ); + assert.equal( + number(18.6), + "19", + "it returns a float number rounded to an integer as a string" + ); + assert.equal( + number("12.3"), + "12", + "it returns a string float rounded to an integer as a string" + ); + assert.equal( + number("12.6"), + "13", + "it returns a string float rounded to an integer as a string" + ); }); QUnit.test("durationTiny", assert => { - assert.equal(durationTiny(), '—', "undefined is a dash"); - assert.equal(durationTiny(null), '—', "null is a dash"); - assert.equal(durationTiny(0), '< 1m', "0 seconds shows as < 1m"); - assert.equal(durationTiny(59), '< 1m', "59 seconds shows as < 1m"); - assert.equal(durationTiny(60), '1m', "60 seconds shows as 1m"); - assert.equal(durationTiny(90), '2m', "90 seconds shows as 2m"); - assert.equal(durationTiny(120), '2m', "120 seconds shows as 2m"); - assert.equal(durationTiny(60 * 45), '1h', "45 minutes shows as 1h"); - assert.equal(durationTiny(60 * 60), '1h', "60 minutes shows as 1h"); - assert.equal(durationTiny(60 * 90), '2h', "90 minutes shows as 2h"); - assert.equal(durationTiny(3600 * 23), '23h', "23 hours shows as 23h"); - assert.equal(durationTiny(3600 * 24 - 29), '1d', "23 hours 31 mins shows as 1d"); - assert.equal(durationTiny(3600 * 24 * 89), '89d', "89 days shows as 89d"); - assert.equal(durationTiny(60 * (525600 - 1)), '12mon', "364 days shows as 12mon"); - assert.equal(durationTiny(60 * 525600), '1y', "365 days shows as 1y"); - assert.equal(durationTiny(86400 * 456), '1y', "456 days shows as 1y"); - assert.equal(durationTiny(86400 * 457), '> 1y', "457 days shows as > 1y"); - assert.equal(durationTiny(86400 * 638), '> 1y', "638 days shows as > 1y"); - assert.equal(durationTiny(86400 * 639), '2y', "639 days shows as 2y"); - assert.equal(durationTiny(86400 * 821), '2y', "821 days shows as 2y"); - assert.equal(durationTiny(86400 * 822), '> 2y', "822 days shows as > 2y"); + assert.equal(durationTiny(), "—", "undefined is a dash"); + assert.equal(durationTiny(null), "—", "null is a dash"); + assert.equal(durationTiny(0), "< 1m", "0 seconds shows as < 1m"); + assert.equal(durationTiny(59), "< 1m", "59 seconds shows as < 1m"); + assert.equal(durationTiny(60), "1m", "60 seconds shows as 1m"); + assert.equal(durationTiny(90), "2m", "90 seconds shows as 2m"); + assert.equal(durationTiny(120), "2m", "120 seconds shows as 2m"); + assert.equal(durationTiny(60 * 45), "1h", "45 minutes shows as 1h"); + assert.equal(durationTiny(60 * 60), "1h", "60 minutes shows as 1h"); + assert.equal(durationTiny(60 * 90), "2h", "90 minutes shows as 2h"); + assert.equal(durationTiny(3600 * 23), "23h", "23 hours shows as 23h"); + assert.equal( + durationTiny(3600 * 24 - 29), + "1d", + "23 hours 31 mins shows as 1d" + ); + assert.equal(durationTiny(3600 * 24 * 89), "89d", "89 days shows as 89d"); + assert.equal( + durationTiny(60 * (525600 - 1)), + "12mon", + "364 days shows as 12mon" + ); + assert.equal(durationTiny(60 * 525600), "1y", "365 days shows as 1y"); + assert.equal(durationTiny(86400 * 456), "1y", "456 days shows as 1y"); + assert.equal(durationTiny(86400 * 457), "> 1y", "457 days shows as > 1y"); + assert.equal(durationTiny(86400 * 638), "> 1y", "638 days shows as > 1y"); + assert.equal(durationTiny(86400 * 639), "2y", "639 days shows as 2y"); + assert.equal(durationTiny(86400 * 821), "2y", "821 days shows as 2y"); + assert.equal(durationTiny(86400 * 822), "> 2y", "822 days shows as > 2y"); }); diff --git a/test/javascripts/lib/i18n-test.js.es6 b/test/javascripts/lib/i18n-test.js.es6 index e653a8d0ca..0f14258901 100644 --- a/test/javascripts/lib/i18n-test.js.es6 +++ b/test/javascripts/lib/i18n-test.js.es6 @@ -7,50 +7,50 @@ QUnit.module("lib:i18n", { I18n.locale = "fr"; I18n.translations = { - "fr_FOO": { - "js": { - "topic": { - "reply": { - "title": "Foo" + fr_FOO: { + js: { + topic: { + reply: { + title: "Foo" } - }, - } - }, - "fr": { - "js": { - "hello": "Bonjour", - "topic": { - "reply": { - "title": "Répondre" - }, - "share": { - "title": "Partager" - } - }, - "character_count": { - "zero": "{{count}} ZERO", - "one": "{{count}} ONE", - "two": "{{count}} TWO", - "few": "{{count}} FEW", - "many": "{{count}} MANY", - "other": "{{count}} OTHER" } } }, - "en": { - "js": { - "hello": { - "world": "Hello World!", - "universe": "" - }, - "topic": { - "reply": { - "help": "begin composing a reply to this topic" + fr: { + js: { + hello: "Bonjour", + topic: { + reply: { + title: "Répondre" + }, + share: { + title: "Partager" } }, - "word_count": { - "one": "1 word", - "other": "{{count}} words" + character_count: { + zero: "{{count}} ZERO", + one: "{{count}} ONE", + two: "{{count}} TWO", + few: "{{count}} FEW", + many: "{{count}} MANY", + other: "{{count}} OTHER" + } + } + }, + en: { + js: { + hello: { + world: "Hello World!", + universe: "" + }, + topic: { + reply: { + help: "begin composing a reply to this topic" + } + }, + word_count: { + one: "1 word", + other: "{{count}} words" } } } @@ -61,7 +61,7 @@ QUnit.module("lib:i18n", { if (n === 0) return "zero"; if (n === 1) return "one"; if (n === 2) return "two"; - if (n >= 3 && n <= 9) return "few"; + if (n >= 3 && n <= 9) return "few"; if (n >= 10 && n <= 99) return "many"; return "other"; }; @@ -80,16 +80,32 @@ QUnit.test("defaults", assert => { }); QUnit.test("translations", assert => { - assert.equal(I18n.t("topic.reply.title"), "Répondre", "uses locale translations when they exist"); - assert.equal(I18n.t("topic.reply.help"), "begin composing a reply to this topic", "fallbacks to English translations"); - assert.equal(I18n.t("hello.world"), "Hello World!", "doesn't break if a key is overriden in a locale"); + assert.equal( + I18n.t("topic.reply.title"), + "Répondre", + "uses locale translations when they exist" + ); + assert.equal( + I18n.t("topic.reply.help"), + "begin composing a reply to this topic", + "fallbacks to English translations" + ); + assert.equal( + I18n.t("hello.world"), + "Hello World!", + "doesn't break if a key is overriden in a locale" + ); assert.equal(I18n.t("hello.universe"), "", "allows empty strings"); }); QUnit.test("extra translations", assert => { - I18n.extras = [{ "admin": { "title": "Discourse Admin" }}]; + I18n.extras = [{ admin: { title: "Discourse Admin" } }]; - assert.equal(I18n.t("admin.title"), "Discourse Admin", "it check extra translations when they exists"); + assert.equal( + I18n.t("admin.title"), + "Discourse Admin", + "it check extra translations when they exists" + ); }); QUnit.test("pluralizations", assert => { @@ -112,7 +128,19 @@ QUnit.test("fallback", assert => { I18n.locale = "fr_FOO"; I18n.fallbackLocale = "fr"; - assert.equal(I18n.t("topic.reply.title"), "Foo", "uses locale translations when they exist"); - assert.equal(I18n.t("topic.share.title"), "Partager", "falls back to fallbackLocale translations when they exist"); - assert.equal(I18n.t("topic.reply.help"), "begin composing a reply to this topic", "falls back to English translations"); + assert.equal( + I18n.t("topic.reply.title"), + "Foo", + "uses locale translations when they exist" + ); + assert.equal( + I18n.t("topic.share.title"), + "Partager", + "falls back to fallbackLocale translations when they exist" + ); + assert.equal( + I18n.t("topic.reply.help"), + "begin composing a reply to this topic", + "falls back to English translations" + ); }); diff --git a/test/javascripts/lib/key-value-store-test.js.es6 b/test/javascripts/lib/key-value-store-test.js.es6 index 1bbafcc5cb..5e21cb8770 100644 --- a/test/javascripts/lib/key-value-store-test.js.es6 +++ b/test/javascripts/lib/key-value-store-test.js.es6 @@ -2,17 +2,17 @@ import KeyValueStore from "discourse/lib/key-value-store"; QUnit.module("lib:key-value-store"); -QUnit.test("it's able to get the result back from the store", (assert) => { +QUnit.test("it's able to get the result back from the store", assert => { const store = new KeyValueStore("_test"); store.set({ key: "bob", value: "uncle" }); assert.equal(store.get("bob"), "uncle"); }); -QUnit.test("is able to nuke the store", (assert) => { +QUnit.test("is able to nuke the store", assert => { const store = new KeyValueStore("_test"); store.set({ key: "bob1", value: "uncle" }); store.abandonLocal(); localStorage.a = 1; assert.equal(store.get("bob1"), void 0); assert.equal(localStorage.a, "1"); -}); \ No newline at end of file +}); diff --git a/test/javascripts/lib/preload-store-test.js.es6 b/test/javascripts/lib/preload-store-test.js.es6 index 26f8d04059..b17d8f20e8 100644 --- a/test/javascripts/lib/preload-store-test.js.es6 +++ b/test/javascripts/lib/preload-store-test.js.es6 @@ -1,78 +1,100 @@ -import PreloadStore from 'preload-store'; +import PreloadStore from "preload-store"; QUnit.module("preload-store", { beforeEach() { - PreloadStore.store('bane', 'evil'); + PreloadStore.store("bane", "evil"); } }); QUnit.test("get", assert => { - assert.blank(PreloadStore.get('joker'), "returns blank for a missing key"); - assert.equal(PreloadStore.get('bane'), 'evil', "returns the value for that key"); + assert.blank(PreloadStore.get("joker"), "returns blank for a missing key"); + assert.equal( + PreloadStore.get("bane"), + "evil", + "returns the value for that key" + ); }); QUnit.test("remove", assert => { - PreloadStore.remove('bane'); - assert.blank(PreloadStore.get('bane'), "removes the value if the key exists"); + PreloadStore.remove("bane"); + assert.blank(PreloadStore.get("bane"), "removes the value if the key exists"); }); -asyncTestDiscourse("getAndRemove returns a promise that resolves to null", function(assert) { +asyncTestDiscourse( + "getAndRemove returns a promise that resolves to null", + function(assert) { + assert.expect(1); + + const done = assert.async(); + PreloadStore.getAndRemove("joker").then(function(result) { + assert.blank(result); + done(); + }); + } +); + +asyncTestDiscourse( + "getAndRemove returns a promise that resolves to the result of the finder", + function(assert) { + assert.expect(1); + + const done = assert.async(); + const finder = function() { + return "batdance"; + }; + PreloadStore.getAndRemove("joker", finder).then(function(result) { + assert.equal(result, "batdance"); + done(); + }); + } +); + +asyncTestDiscourse( + "getAndRemove returns a promise that resolves to the result of the finder's promise", + function(assert) { + assert.expect(1); + + const finder = function() { + return new Ember.RSVP.Promise(function(resolve) { + resolve("hahahah"); + }); + }; + + const done = assert.async(); + PreloadStore.getAndRemove("joker", finder).then(function(result) { + assert.equal(result, "hahahah"); + done(); + }); + } +); + +asyncTestDiscourse( + "returns a promise that rejects with the result of the finder's rejected promise", + function(assert) { + assert.expect(1); + + const finder = function() { + return new Ember.RSVP.Promise(function(resolve, reject) { + reject("error"); + }); + }; + + const done = assert.async(); + PreloadStore.getAndRemove("joker", finder).then(null, function(result) { + assert.equal(result, "error"); + done(); + }); + } +); + +asyncTestDiscourse("returns a promise that resolves to 'evil'", function( + assert +) { assert.expect(1); const done = assert.async(); - PreloadStore.getAndRemove('joker').then(function(result) { - assert.blank(result); - done(); - }); -}); - -asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder", function(assert) { - assert.expect(1); - - const done = assert.async(); - const finder = function() { return 'batdance'; }; - PreloadStore.getAndRemove('joker', finder).then(function(result) { - assert.equal(result, 'batdance'); - done(); - }); - -}); - -asyncTestDiscourse("getAndRemove returns a promise that resolves to the result of the finder's promise", function(assert) { - assert.expect(1); - - const finder = function() { - return new Ember.RSVP.Promise(function(resolve) { resolve('hahahah'); }); - }; - - const done = assert.async(); - PreloadStore.getAndRemove('joker', finder).then(function(result) { - assert.equal(result, 'hahahah'); - done(); - }); -}); - -asyncTestDiscourse("returns a promise that rejects with the result of the finder's rejected promise", function(assert) { - assert.expect(1); - - const finder = function() { - return new Ember.RSVP.Promise(function(resolve, reject) { reject('error'); }); - }; - - const done = assert.async(); - PreloadStore.getAndRemove('joker', finder).then(null, function(result) { - assert.equal(result, 'error'); - done(); - }); - -}); - -asyncTestDiscourse("returns a promise that resolves to 'evil'", function(assert) { - assert.expect(1); - - const done = assert.async(); - PreloadStore.getAndRemove('bane').then(function(result) { - assert.equal(result, 'evil'); + PreloadStore.getAndRemove("bane").then(function(result) { + assert.equal(result, "evil"); done(); }); }); diff --git a/test/javascripts/lib/pretty-text-test.js.es6 b/test/javascripts/lib/pretty-text-test.js.es6 index 892a9aca49..c7313b99da 100644 --- a/test/javascripts/lib/pretty-text-test.js.es6 +++ b/test/javascripts/lib/pretty-text-test.js.es6 @@ -1,7 +1,7 @@ -import Quote from 'discourse/lib/quote'; -import Post from 'discourse/models/post'; -import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text'; -import { IMAGE_VERSION as v} from 'pretty-text/emoji'; +import Quote from "discourse/lib/quote"; +import Post from "discourse/models/post"; +import { default as PrettyText, buildOptions } from "pretty-text/pretty-text"; +import { IMAGE_VERSION as v } from "pretty-text/emoji"; QUnit.module("lib:pretty-text"); @@ -10,13 +10,13 @@ const rawOpts = { enable_emoji: true, enable_emoji_shortcuts: true, enable_mentions: true, - emoji_set: 'emoji_one', - highlighted_languages: 'json|ruby|javascript', - default_code_lang: 'auto', + emoji_set: "emoji_one", + highlighted_languages: "json|ruby|javascript", + default_code_lang: "auto", enable_markdown_linkify: true, - markdown_linkify_tlds: 'com' + markdown_linkify_tlds: "com" }, - censoredWords: 'shucks|whiz|whizzer|a**le|badword*', + censoredWords: "shucks|whiz|whizzer|a**le|badword*", getURL: url => url }; @@ -47,18 +47,31 @@ QUnit.assert.cookedPara = function(input, expected, message) { QUnit.assert.cooked(input, `

      ${expected}

      `, message); }; - QUnit.skip("Pending Engine fixes and spec fixes", assert => { - assert.cooked("Derpy: http://derp.com?_test_=1", - '

      Derpy: http://derp.com?_test_=1

      ', - "works with underscores in urls"); + assert.cooked( + "Derpy: http://derp.com?_test_=1", + '

      Derpy: http://derp.com?_test_=1

      ', + "works with underscores in urls" + ); - assert.cooked("**a*_b**", "

      a*_b

      ", "allows for characters within bold"); + assert.cooked( + "**a*_b**", + "

      a*_b

      ", + "allows for characters within bold" + ); }); QUnit.test("buildOptions", assert => { - assert.ok(buildOptions({ siteSettings: { enable_emoji: true } }).discourse.features.emoji, 'emoji enabled'); - assert.ok(!buildOptions({ siteSettings: { enable_emoji: false } }).discourse.features.emoji, 'emoji disabled'); + assert.ok( + buildOptions({ siteSettings: { enable_emoji: true } }).discourse.features + .emoji, + "emoji enabled" + ); + assert.ok( + !buildOptions({ siteSettings: { enable_emoji: false } }).discourse.features + .emoji, + "emoji disabled" + ); }); QUnit.test("basic cooking", assert => { @@ -67,151 +80,275 @@ QUnit.test("basic cooking", assert => { assert.cooked("__bold__", "

      bold

      ", "it bolds text."); assert.cooked("*trout*", "

      trout

      ", "it italicizes text."); assert.cooked("_trout_", "

      trout

      ", "it italicizes text."); - assert.cooked("***hello***", "

      hello

      ", "it can do bold and italics at once."); - assert.cooked("word_with_underscores", "

      word_with_underscores

      ", "it doesn't do intraword italics"); - assert.cooked("common/_special_font_face.html.erb", "

      common/_special_font_face.html.erb

      ", "it doesn't intraword with a slash"); - assert.cooked("hello \\*evil\\*", "

      hello *evil*

      ", "it supports escaping of asterisks"); - assert.cooked("hello \\_evil\\_", "

      hello _evil_

      ", "it supports escaping of italics"); - assert.cooked("brussels sprouts are *awful*.", "

      brussels sprouts are awful.

      ", "it doesn't swallow periods."); + assert.cooked( + "***hello***", + "

      hello

      ", + "it can do bold and italics at once." + ); + assert.cooked( + "word_with_underscores", + "

      word_with_underscores

      ", + "it doesn't do intraword italics" + ); + assert.cooked( + "common/_special_font_face.html.erb", + "

      common/_special_font_face.html.erb

      ", + "it doesn't intraword with a slash" + ); + assert.cooked( + "hello \\*evil\\*", + "

      hello *evil*

      ", + "it supports escaping of asterisks" + ); + assert.cooked( + "hello \\_evil\\_", + "

      hello _evil_

      ", + "it supports escaping of italics" + ); + assert.cooked( + "brussels sprouts are *awful*.", + "

      brussels sprouts are awful.

      ", + "it doesn't swallow periods." + ); }); QUnit.test("Nested bold and italics", assert => { - assert.cooked("*this is italic **with some bold** inside*", "

      this is italic with some bold inside

      ", "it handles nested bold in italics"); + assert.cooked( + "*this is italic **with some bold** inside*", + "

      this is italic with some bold inside

      ", + "it handles nested bold in italics" + ); }); QUnit.test("Traditional Line Breaks", assert => { const input = "1\n2\n3"; - assert.cooked(input, "

      1
      \n2
      \n3

      ", "automatically handles trivial newlines"); - assert.cookedOptions(input, { siteSettings: {traditional_markdown_linebreaks: true} }, "

      1\n2\n3

      "); + assert.cooked( + input, + "

      1
      \n2
      \n3

      ", + "automatically handles trivial newlines" + ); + assert.cookedOptions( + input, + { siteSettings: { traditional_markdown_linebreaks: true } }, + "

      1\n2\n3

      " + ); }); QUnit.test("Unbalanced underscores", assert => { - assert.cooked("[evil_trout][1] hello_\n\n[1]: http://eviltrout.com", "

      evil_trout hello_

      "); + assert.cooked( + "[evil_trout][1] hello_\n\n[1]: http://eviltrout.com", + '

      evil_trout hello_

      ' + ); }); QUnit.test("Line Breaks", assert => { - assert.cooked("[] first choice\n[] second choice", - "

      [] first choice
      \n[] second choice

      ", - "it handles new lines correctly with [] options"); + assert.cooked( + "[] first choice\n[] second choice", + "

      [] first choice
      \n[] second choice

      ", + "it handles new lines correctly with [] options" + ); // note this is a change from previous engine but is correct // we have an html block and behavior is defined per common mark // spec // ole engine would wrap trout in a

      - assert.cooked("

      evil
      \ntrout", - "
      evil
      \ntrout", - "it doesn't insert
      after blockquotes"); + assert.cooked( + "
      evil
      \ntrout", + "
      evil
      \ntrout", + "it doesn't insert
      after blockquotes" + ); - assert.cooked("leading
      evil
      \ntrout", - "

      leading

      evil

      \ntrout

      ", - "it doesn't insert
      after blockquotes with leading text"); + assert.cooked( + "leading
      evil
      \ntrout", + "

      leading

      evil

      \ntrout

      ", + "it doesn't insert
      after blockquotes with leading text" + ); }); QUnit.test("Paragraphs for HTML", assert => { - assert.cooked("
      hello world
      ", "
      hello world
      ", "it doesn't surround
      with paragraphs"); - assert.cooked("

      hello world

      ", "

      hello world

      ", "it doesn't surround

      with paragraphs"); - assert.cooked("hello world", "

      hello world

      ", "it surrounds inline html tags with paragraphs"); - assert.cooked("hello world", "

      hello world

      ", "it surrounds inline html tags with paragraphs"); + assert.cooked( + "
      hello world
      ", + "
      hello world
      ", + "it doesn't surround
      with paragraphs" + ); + assert.cooked( + "

      hello world

      ", + "

      hello world

      ", + "it doesn't surround

      with paragraphs" + ); + assert.cooked( + "hello world", + "

      hello world

      ", + "it surrounds inline html tags with paragraphs" + ); + assert.cooked( + "hello world", + "

      hello world

      ", + "it surrounds inline html tags with paragraphs" + ); }); QUnit.test("Links", assert => { + assert.cooked( + "EvilTrout: http://eviltrout.com", + '

      EvilTrout: http://eviltrout.com

      ', + "autolinks a URL" + ); - assert.cooked("EvilTrout: http://eviltrout.com", - '

      EvilTrout: http://eviltrout.com

      ', - "autolinks a URL"); + assert.cooked( + "Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A", + '

      Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A

      ', + "allows links to contain query params" + ); - assert.cooked("Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A", - '

      Youtube: http://www.youtube.com/watch?v=1MrpeBRkM5A

      ', - "allows links to contain query params"); + assert.cooked( + "Derpy: http://derp.com?__test=1", + '

      Derpy: http://derp.com?__test=1

      ', + "works with double underscores in urls" + ); - assert.cooked("Derpy: http://derp.com?__test=1", - '

      Derpy: http://derp.com?__test=1

      ', - "works with double underscores in urls"); + assert.cooked( + "Atwood: www.codinghorror.com", + '

      Atwood: www.codinghorror.com

      ', + "autolinks something that begins with www" + ); - assert.cooked("Atwood: www.codinghorror.com", - '

      Atwood: www.codinghorror.com

      ', - "autolinks something that begins with www"); + assert.cooked( + "Atwood: http://www.codinghorror.com", + '

      Atwood: http://www.codinghorror.com

      ', + "autolinks a URL with http://www" + ); - assert.cooked("Atwood: http://www.codinghorror.com", - '

      Atwood: http://www.codinghorror.com

      ', - "autolinks a URL with http://www"); + assert.cooked( + "EvilTrout: http://eviltrout.com hello", + '

      EvilTrout: http://eviltrout.com hello

      ', + "autolinks with trailing text" + ); - assert.cooked("EvilTrout: http://eviltrout.com hello", - '

      EvilTrout: http://eviltrout.com hello

      ', - "autolinks with trailing text"); + assert.cooked( + "here is [an example](http://twitter.com)", + '

      here is an example

      ', + "supports markdown style links" + ); - assert.cooked("here is [an example](http://twitter.com)", - '

      here is an example

      ', - "supports markdown style links"); + assert.cooked( + "Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)", + '

      Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)

      ', + "autolinks a URL with parentheses (like Wikipedia)" + ); - assert.cooked("Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)", - '

      Batman: http://en.wikipedia.org/wiki/The_Dark_Knight_(film)

      ', - "autolinks a URL with parentheses (like Wikipedia)"); + assert.cooked( + "Here's a tweet:\nhttps://twitter.com/evil_trout/status/345954894420787200", + '

      Here\'s a tweet:
      \nhttps://twitter.com/evil_trout/status/345954894420787200

      ', + "It doesn't strip the new line." + ); - assert.cooked("Here's a tweet:\nhttps://twitter.com/evil_trout/status/345954894420787200", - "

      Here's a tweet:
      \nhttps://twitter.com/evil_trout/status/345954894420787200

      ", - "It doesn't strip the new line."); + assert.cooked( + "1. View @eviltrout's profile here: http://meta.discourse.org/u/eviltrout/activity
      next line.", + '
        \n
      1. View @eviltrout\'s profile here: http://meta.discourse.org/u/eviltrout/activity
        next line.
      2. \n
      ', + "allows autolinking within a list without inserting a paragraph." + ); - assert.cooked("1. View @eviltrout's profile here: http://meta.discourse.org/u/eviltrout/activity
      next line.", - "
        \n
      1. View @eviltrout's profile here: http://meta.discourse.org/u/eviltrout/activity
        next line.
      2. \n
      ", - "allows autolinking within a list without inserting a paragraph."); + assert.cooked( + "[3]: http://eviltrout.com", + "", + "It doesn't autolink markdown link references" + ); - assert.cooked("[3]: http://eviltrout.com", "", "It doesn't autolink markdown link references"); + assert.cooked( + "[]: http://eviltrout.com", + '

      []: http://eviltrout.com

      ', + "It doesn't accept empty link references" + ); - assert.cooked("[]: http://eviltrout.com", "

      []: http://eviltrout.com

      ", "It doesn't accept empty link references"); + assert.cooked( + "[b]label[/b]: description", + '

      label: description

      ', + "It doesn't accept BBCode as link references" + ); - assert.cooked("[b]label[/b]: description", "

      label: description

      ", "It doesn't accept BBCode as link references"); + assert.cooked( + "http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369", + '

      http://discourse.org and ' + + 'http://discourse.org/another_url and ' + + 'http://www.imdb.com/name/nm2225369

      ', + "allows multiple links on one line" + ); - assert.cooked("http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369", - "

      http://discourse.org and " + - "http://discourse.org/another_url and " + - "http://www.imdb.com/name/nm2225369

      ", - 'allows multiple links on one line'); + assert.cooked( + "* [Evil Trout][1]\n\n[1]: http://eviltrout.com", + '', + "allows markdown link references in a list" + ); - assert.cooked("* [Evil Trout][1]\n\n[1]: http://eviltrout.com", - "", - "allows markdown link references in a list"); + assert.cooked( + "User [MOD]: Hello!", + "

      User [MOD]: Hello!

      ", + "It does not consider references that are obviously not URLs" + ); - assert.cooked("User [MOD]: Hello!", - "

      User [MOD]: Hello!

      ", - "It does not consider references that are obviously not URLs"); + assert.cooked( + "http://eviltrout.com", + '

      http://eviltrout.com

      ', + "Links within HTML tags" + ); - assert.cooked("http://eviltrout.com", "

      http://eviltrout.com

      ", "Links within HTML tags"); + assert.cooked( + "[http://google.com ... wat](http://discourse.org)", + '

      http://google.com ... wat

      ', + "it supports links within links" + ); - assert.cooked("[http://google.com ... wat](http://discourse.org)", - "

      http://google.com ... wat

      ", - "it supports links within links"); + assert.cooked( + "[http://google.com](http://discourse.org)", + '

      http://google.com

      ', + "it supports markdown links where the name and link match" + ); - assert.cooked("[http://google.com](http://discourse.org)", - "

      http://google.com

      ", - "it supports markdown links where the name and link match"); + assert.cooked( + '[Link](http://www.example.com) (with an outer "description")', + '

      Link (with an outer "description")

      ', + "it doesn't consume closing parens as part of the url" + ); + assert.cooked( + "A link inside parentheses (http://www.example.com)", + '

      A link inside parentheses (http://www.example.com)

      ', + "it auto-links a url within parentheses" + ); - assert.cooked("[Link](http://www.example.com) (with an outer \"description\")", - "

      Link (with an outer "description")

      ", - "it doesn't consume closing parens as part of the url"); - - assert.cooked("A link inside parentheses (http://www.example.com)", - "

      A link inside parentheses (http://www.example.com)

      ", - "it auto-links a url within parentheses"); - - assert.cooked("[ul][1]\n\n[1]: http://eviltrout.com", - "

      ul

      ", - "it can use `ul` as a link name"); + assert.cooked( + "[ul][1]\n\n[1]: http://eviltrout.com", + '

      ul

      ', + "it can use `ul` as a link name" + ); }); QUnit.test("simple quotes", assert => { - assert.cooked("> nice!", "
      \n

      nice!

      \n
      ", "it supports simple quotes"); - assert.cooked(" > nice!", "
      \n

      nice!

      \n
      ", "it allows quotes with preceding spaces"); - assert.cooked("> level 1\n> > level 2", - "
      \n

      level 1

      \n
      \n

      level 2

      \n
      \n
      ", - "it allows nesting of blockquotes"); - assert.cooked("> level 1\n> > level 2", - "
      \n

      level 1

      \n
      \n

      level 2

      \n
      \n
      ", - "it allows nesting of blockquotes with spaces"); + assert.cooked( + "> nice!", + "
      \n

      nice!

      \n
      ", + "it supports simple quotes" + ); + assert.cooked( + " > nice!", + "
      \n

      nice!

      \n
      ", + "it allows quotes with preceding spaces" + ); + assert.cooked( + "> level 1\n> > level 2", + "
      \n

      level 1

      \n
      \n

      level 2

      \n
      \n
      ", + "it allows nesting of blockquotes" + ); + assert.cooked( + "> level 1\n> > level 2", + "
      \n

      level 1

      \n
      \n

      level 2

      \n
      \n
      ", + "it allows nesting of blockquotes with spaces" + ); - assert.cooked("- hello\n\n > world\n > eviltrout", -`
        + assert.cooked( + "- hello\n\n > world\n > eviltrout", + `
        • hello

          @@ -220,21 +357,26 @@ eviltrout

        `, - "it allows quotes within a list."); + "it allows quotes within a list." + ); - assert.cooked("-

        eviltrout

        ", - "
          \n
        • \n

          eviltrout

        • \n
        ", - "it allows paragraphs within a list."); - - - assert.cooked(" > indent 1\n > indent 2", "
        \n

        indent 1
        \nindent 2

        \n
        ", "allow multiple spaces to indent"); + assert.cooked( + "-

        eviltrout

        ", + "
          \n
        • \n

          eviltrout

        • \n
        ", + "it allows paragraphs within a list." + ); + assert.cooked( + " > indent 1\n > indent 2", + "
        \n

        indent 1
        \nindent 2

        \n
        ", + "allow multiple spaces to indent" + ); }); QUnit.test("Quotes", assert => { - - assert.cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n\nthird line\n[/quote]", - { topicId: 2 }, + assert.cookedOptions( + '[quote="eviltrout, post: 1"]\na quote\n\nsecond line\n\nthird line\n[/quote]', + { topicId: 2 }, ``, - "works with multiple lines"); + "works with multiple lines" + ); - assert.cookedOptions("[quote=\"bob, post:1\"]\nmy quote\n[/quote]", - { topicId: 2, lookupAvatar: function() { } }, + assert.cookedOptions( + '[quote="bob, post:1"]\nmy quote\n[/quote]', + { topicId: 2, lookupAvatar: function() {} }, ``, - "includes no avatar if none is found"); + "includes no avatar if none is found" + ); - assert.cooked(`[quote]\na\n\n[quote]\nb\n[/quote]\n[/quote]`, + assert.cooked( + `[quote]\na\n\n[quote]\nb\n[/quote]\n[/quote]`, ` `, - "handles nested quotes properly"); + "handles nested quotes properly" + ); - assert.cookedOptions(`[quote="bob, post:1, topic:1"]\ntest quote\n[/quote]`, { lookupPrimaryUserGroupByPostNumber: () => "aUserGroup" }, - `
      "), "
      "); - assert.equal(pt.sanitize("

      hello

      "), "

      hello

      "); + assert.equal(pt.sanitize('bug'), "bug"); + assert.equal( + pt.sanitize("
      "), + "
      " + ); + assert.equal( + pt.sanitize("

      hello

      "), + "

      hello

      " + ); assert.equal(pt.sanitize("<3 <3"), "<3 <3"); assert.equal(pt.sanitize("<_<"), "<_<"); - cooked("hello", "

      hello

      ", "it sanitizes while cooking"); + cooked( + "hello", + "

      hello

      ", + "it sanitizes while cooking" + ); - cooked("disney reddit", - "

      disney reddit

      ", - "we can embed proper links"); + cooked( + "disney reddit", + '

      disney reddit

      ', + "we can embed proper links" + ); cooked("
      hello
      ", "hello", "it does not allow centering"); - cooked("
      a\n
      \n", "
      a\n
      ", "it does not double sanitize"); + cooked( + "
      a\n
      \n", + "
      a\n
      ", + "it does not double sanitize" + ); - cooked("", "", "it does not allow most iframes"); + cooked( + '', + "", + "it does not allow most iframes" + ); - cooked("", - "", - "it allows iframe to google maps"); + cooked( + '', + '', + "it allows iframe to google maps" + ); - cooked("", - "", - "it allows iframe to OpenStreetMap"); + cooked( + '', + '', + "it allows iframe to OpenStreetMap" + ); assert.equal(pt.sanitize(""), "hullo"); assert.equal(pt.sanitize(""), "press me!"); @@ -40,24 +70,62 @@ QUnit.test("sanitize", assert => { assert.equal(pt.sanitize("hello"), "hello"); assert.equal(pt.sanitize("highlight"), "highlight"); - cooked("[the answer](javascript:alert(42))", "

      [the answer](javascript:alert(42))

      ", "it prevents XSS"); + cooked( + "[the answer](javascript:alert(42))", + "

      [the answer](javascript:alert(42))

      ", + "it prevents XSS" + ); - cooked("\n", "

      ", "it doesn't circumvent XSS with comments"); + cooked( + '\n', + "

      ", + "it doesn't circumvent XSS with comments" + ); - cooked("a", "

      a

      ", "it sanitizes spans"); - cooked("a", "

      a

      ", "it sanitizes spans"); - cooked("a", "

      a

      ", "it sanitizes spans"); + cooked( + 'a', + "

      a

      ", + "it sanitizes spans" + ); + cooked( + 'a', + "

      a

      ", + "it sanitizes spans" + ); + cooked( + 'a', + '

      a

      ', + "it sanitizes spans" + ); cooked("Ctrl+C", "

      Ctrl+C

      "); - cooked("it has been 1 day 0 days since our last test failure", "

      it has been 1 day 0 days since our last test failure

      "); - cooked(`it has been 1 day 0 days since our last test failure`, `

      it has been 1 day 0 days since our last test failure

      `); + cooked( + "it has been 1 day 0 days since our last test failure", + "

      it has been 1 day 0 days since our last test failure

      " + ); + cooked( + `it has been 1 day 0 days since our last test failure`, + `

      it has been 1 day 0 days since our last test failure

      ` + ); cooked(`
      hello
      `, `
      hello
      `); - cooked(`1 + 1 is 3 2`, `

      1 + 1 is 3 2

      `); - cooked(`JS`, `

      JS

      `); - cooked(`
      Forum
      Software
      `, `
      Forum
      Software
      `); - cooked(`high low HUGE`, `

      high low HUGE

      `); + cooked( + `1 + 1 is 3 2`, + `

      1 + 1 is 3 2

      ` + ); + cooked( + `JS`, + `

      JS

      ` + ); + cooked( + `
      Forum
      Software
      `, + `
      Forum
      Software
      ` + ); + cooked( + `high low HUGE`, + `

      high low HUGE

      ` + ); cooked(`
      RTL text
      `, `
      RTL text
      `); }); @@ -65,22 +133,58 @@ QUnit.test("sanitize", assert => { QUnit.test("ids on headings", assert => { const pt = new PrettyText(buildOptions({ siteSettings: {} })); assert.equal(pt.sanitize("

      Test Heading

      "), "

      Test Heading

      "); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`
      Test Heading
      `), `
      Test Heading
      `); - assert.equal(pt.sanitize(`
      Test Heading
      `), `
      Test Heading
      `); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`
      Test Heading
      `), + `
      Test Heading
      ` + ); + assert.equal( + pt.sanitize(`
      Test Heading
      `), + `
      Test Heading
      ` + ); }); QUnit.test("poorly formed ids on headings", assert => { let pt = new PrettyText(buildOptions({ siteSettings: {} })); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); - assert.equal(pt.sanitize(`

      Test Heading

      `), `

      Test Heading

      `); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); + assert.equal( + pt.sanitize(`

      Test Heading

      `), + `

      Test Heading

      ` + ); }); QUnit.test("urlAllowed", assert => { @@ -91,8 +195,9 @@ QUnit.test("urlAllowed", assert => { allowed("https://eviltrout.com/evil/trout", "allows https urls"); allowed("//eviltrout.com/evil/trout", "allows protocol relative urls"); - assert.equal(hrefAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"), - "http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf", - "escape single quotes"); + assert.equal( + hrefAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"), + "http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf", + "escape single quotes" + ); }); - diff --git a/test/javascripts/lib/sharing-test.js.es6 b/test/javascripts/lib/sharing-test.js.es6 new file mode 100644 index 0000000000..2a6f249b78 --- /dev/null +++ b/test/javascripts/lib/sharing-test.js.es6 @@ -0,0 +1,42 @@ +import Sharing from "discourse/lib/sharing"; + +QUnit.module("lib:sharing", { + beforeEach() { + Sharing._reset(); + } +}); + +QUnit.test("addSource", assert => { + const sharingSettings = "facebook|twitter"; + + assert.blank(Sharing.activeSources(sharingSettings)); + + Sharing.addSource({ + id: "facebook" + }); + + assert.equal(Sharing.activeSources(sharingSettings).length, 1); +}); + +QUnit.test("addSharingId", assert => { + const sharingSettings = ""; + + assert.blank(Sharing.activeSources(sharingSettings)); + + Sharing.addSource({ + id: "new-source" + }); + + assert.blank( + Sharing.activeSources(sharingSettings), + "it doesn’t activate a source not in settings" + ); + + Sharing.addSharingId("new-source"); + + assert.equal( + Sharing.activeSources(sharingSettings).length, + 1, + "it adds sharing id to existing sharing settings" + ); +}); diff --git a/test/javascripts/lib/text-direction-test.js.es6 b/test/javascripts/lib/text-direction-test.js.es6 index c8ddee262b..b266af0165 100644 --- a/test/javascripts/lib/text-direction-test.js.es6 +++ b/test/javascripts/lib/text-direction-test.js.es6 @@ -1,23 +1,22 @@ -import { isRTL, isLTR } from 'discourse/lib/text-direction'; +import { isRTL, isLTR } from "discourse/lib/text-direction"; -QUnit.module('lib:text-direction'); +QUnit.module("lib:text-direction"); QUnit.test("isRTL", assert => { // Hebrew - assert.equal(isRTL('זה מבחן'), true); + assert.equal(isRTL("זה מבחן"), true); // Arabic - assert.equal(isRTL('هذا اختبار'), true); + assert.equal(isRTL("هذا اختبار"), true); // Persian - assert.equal(isRTL('این یک امتحان است'), true); + assert.equal(isRTL("این یک امتحان است"), true); - assert.equal(isRTL('This is a test'), false); - assert.equal(isRTL(''), false); + assert.equal(isRTL("This is a test"), false); + assert.equal(isRTL(""), false); }); QUnit.test("isLTR", assert => { - assert.equal(isLTR('This is a test'), true); - assert.equal(isLTR('זה מבחן'), false); + assert.equal(isLTR("This is a test"), true); + assert.equal(isLTR("זה מבחן"), false); }); - diff --git a/test/javascripts/lib/to-markdown-test.js.es6 b/test/javascripts/lib/to-markdown-test.js.es6 index 6336da48ad..ec59e164a6 100644 --- a/test/javascripts/lib/to-markdown-test.js.es6 +++ b/test/javascripts/lib/to-markdown-test.js.es6 @@ -1,4 +1,4 @@ -import toMarkdown from 'discourse/lib/to-markdown'; +import toMarkdown from "discourse/lib/to-markdown"; QUnit.module("lib:to-markdown"); @@ -119,31 +119,34 @@ QUnit.test("converts table tags", assert => { assert.equal(toMarkdown(html), markdown); }); -QUnit.test("replace pipes with spaces if table format not supported", assert => { - let html = ` +QUnit.test( + "replace pipes with spaces if table format not supported", + assert => { + let html = `
      Headi

      ng 1
      Head 2
      Loremipsum
      sit amet
      `; - let markdown = `Headi\n\nng 1 Head 2\nLorem ipsum\n[![](http://dolor.com/image.png)](http://example.com) *sit amet*`; - assert.equal(toMarkdown(html), markdown); + let markdown = `Headi\n\nng 1 Head 2\nLorem ipsum\n[![](http://dolor.com/image.png)](http://example.com) *sit amet*`; + assert.equal(toMarkdown(html), markdown); - html = ` + html = `
      Heading 1
      Lorem
      sit amet
      `; - markdown = `Heading 1\nLorem\n*sit amet*`; - assert.equal(toMarkdown(html), markdown); + markdown = `Heading 1\nLorem\n*sit amet*`; + assert.equal(toMarkdown(html), markdown); - html = `
      Loremsit amet
      `; - markdown = `Lorem **sit amet**`; - assert.equal(toMarkdown(html), markdown); -}); + html = `
      Loremsit amet
      `; + markdown = `Lorem **sit amet**`; + assert.equal(toMarkdown(html), markdown); + } +); QUnit.test("converts img tag", assert => { const url = "https://example.com/image.png"; @@ -154,10 +157,16 @@ QUnit.test("converts img tag", assert => { assert.equal(toMarkdown(html), `![description|50x100](${url})`); html = `description`; - assert.equal(toMarkdown(html), `[![description](${url})](http://example.com)`); + assert.equal( + toMarkdown(html), + `[![description](${url})](http://example.com)` + ); html = `description `; - assert.equal(toMarkdown(html), `[description ![](${url})](http://example.com)`); + assert.equal( + toMarkdown(html), + `[description ![](${url})](http://example.com)` + ); html = `description`; assert.equal(toMarkdown(html), ""); @@ -167,7 +176,8 @@ QUnit.test("converts img tag", assert => { }); QUnit.test("supporting html tags by keeping them", assert => { - let html = "Lorem ipsum dolor sit amet, consectetur"; + let html = + "Lorem ipsum dolor sit amet, consectetur"; let output = html; assert.equal(toMarkdown(html), output); @@ -216,11 +226,13 @@ QUnit.test("converts blockquote tag", assert => { let output = "> Lorem ipsum"; assert.equal(toMarkdown(html), output); - html = "
      Lorem ipsum

      dolor sit amet

      "; + html = + "
      Lorem ipsum

      dolor sit amet

      "; output = "> Lorem ipsum\n\n> dolor sit amet"; assert.equal(toMarkdown(html), output); - html = "
      \nLorem ipsum\n

      dolor

      sit
      amet

      "; + html = + "
      \nLorem ipsum\n

      dolor

      sit
      amet

      "; output = "> Lorem ipsum\n> > dolor\n> > > sit\n> > amet"; assert.equal(toMarkdown(html), output); }); diff --git a/test/javascripts/lib/url-test.js.es6 b/test/javascripts/lib/url-test.js.es6 index 15f760d92d..993fe8c23f 100644 --- a/test/javascripts/lib/url-test.js.es6 +++ b/test/javascripts/lib/url-test.js.es6 @@ -1,4 +1,4 @@ -import { default as DiscourseURL, userPath } from 'discourse/lib/url'; +import { default as DiscourseURL, userPath } from "discourse/lib/url"; QUnit.module("lib:url"); @@ -7,34 +7,61 @@ QUnit.test("isInternal with a HTTP url", assert => { assert.not(DiscourseURL.isInternal(null), "a blank URL is not internal"); assert.ok(DiscourseURL.isInternal("/test"), "relative URLs are internal"); - assert.ok(DiscourseURL.isInternal("//eviltrout.com"), "a url on the same host is internal (protocol-less)"); - assert.ok(DiscourseURL.isInternal("http://eviltrout.com/tophat"), "a url on the same host is internal"); - assert.ok(DiscourseURL.isInternal("https://eviltrout.com/moustache"), "a url on a HTTPS of the same host is internal"); - assert.not(DiscourseURL.isInternal("//twitter.com.com"), "a different host is not internal (protocol-less)"); - assert.not(DiscourseURL.isInternal("http://twitter.com"), "a different host is not internal"); + assert.ok( + DiscourseURL.isInternal("//eviltrout.com"), + "a url on the same host is internal (protocol-less)" + ); + assert.ok( + DiscourseURL.isInternal("http://eviltrout.com/tophat"), + "a url on the same host is internal" + ); + assert.ok( + DiscourseURL.isInternal("https://eviltrout.com/moustache"), + "a url on a HTTPS of the same host is internal" + ); + assert.not( + DiscourseURL.isInternal("//twitter.com.com"), + "a different host is not internal (protocol-less)" + ); + assert.not( + DiscourseURL.isInternal("http://twitter.com"), + "a different host is not internal" + ); }); QUnit.test("isInternal with a HTTPS url", assert => { sandbox.stub(DiscourseURL, "origin").returns("https://eviltrout.com"); - assert.ok(DiscourseURL.isInternal("http://eviltrout.com/monocle"), "HTTPS urls match HTTP urls"); + assert.ok( + DiscourseURL.isInternal("http://eviltrout.com/monocle"), + "HTTPS urls match HTTP urls" + ); }); QUnit.test("isInternal on subfolder install", assert => { sandbox.stub(DiscourseURL, "origin").returns("http://eviltrout.com/forum"); - assert.not(DiscourseURL.isInternal("http://eviltrout.com"), "the host root is not internal"); - assert.not(DiscourseURL.isInternal("http://eviltrout.com/tophat"), "a url on the same host but on a different folder is not internal"); - assert.ok(DiscourseURL.isInternal("http://eviltrout.com/forum/moustache"), "a url on the same host and on the same folder is internal"); + assert.not( + DiscourseURL.isInternal("http://eviltrout.com"), + "the host root is not internal" + ); + assert.not( + DiscourseURL.isInternal("http://eviltrout.com/tophat"), + "a url on the same host but on a different folder is not internal" + ); + assert.ok( + DiscourseURL.isInternal("http://eviltrout.com/forum/moustache"), + "a url on the same host and on the same folder is internal" + ); }); QUnit.test("userPath", assert => { - assert.equal(userPath(), '/u'); - assert.equal(userPath('eviltrout'), '/u/eviltrout'); - assert.equal(userPath('hp.json'), '/u/hp.json'); + assert.equal(userPath(), "/u"); + assert.equal(userPath("eviltrout"), "/u/eviltrout"); + assert.equal(userPath("hp.json"), "/u/hp.json"); }); QUnit.test("userPath with BaseUri", assert => { Discourse.BaseUri = "/forum"; - assert.equal(userPath(), '/forum/u'); - assert.equal(userPath('eviltrout'), '/forum/u/eviltrout'); - assert.equal(userPath('hp.json'), '/forum/u/hp.json'); + assert.equal(userPath(), "/forum/u"); + assert.equal(userPath("eviltrout"), "/forum/u/eviltrout"); + assert.equal(userPath("hp.json"), "/forum/u/hp.json"); }); diff --git a/test/javascripts/lib/user-search-test.js.es6 b/test/javascripts/lib/user-search-test.js.es6 index a34f114395..068b648627 100644 --- a/test/javascripts/lib/user-search-test.js.es6 +++ b/test/javascripts/lib/user-search-test.js.es6 @@ -1,67 +1,68 @@ -import userSearch from 'discourse/lib/user-search'; +import userSearch from "discourse/lib/user-search"; QUnit.module("lib:user-search", { beforeEach() { - const response = (object) => { - return [ - 200, - {"Content-Type": "application/json"}, - object - ]; + const response = object => { + return [200, { "Content-Type": "application/json" }, object]; }; - server.get('/u/search/users', () => { //eslint-disable-line - return response( - { - users: [ - { - "username": "TeaMoe", - "name": "TeaMoe", - "avatar_template": "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png" - }, - { - "username": "TeamOneJ", - "name": "J Cobb", - "avatar_template": + // prettier-ignore + server.get("/u/search/users", () => { //eslint-disable-line + return response({ + users: [ + { + username: "TeaMoe", + name: "TeaMoe", + avatar_template: + "https://avatars.discourse.org/v3/letter/t/41988e/{size}.png" + }, + { + username: "TeamOneJ", + name: "J Cobb", + avatar_template: "https://avatars.discourse.org/v3/letter/t/3d9bf3/{size}.png" - }, - { - "username": "kudos", - "name": "Team Blogeto.com", - "avatar_template": "/user_avatar/meta.discourse.org/kudos/{size}/62185_1.png" - }, - { - "username": "RosieLinda", - "name": "Linda Teaman", - "avatar_template": "https://avatars.discourse.org/v3/letter/r/bc8723/{size}.png" - }, - { - "username": "legalatom", - "name": "Team LegalAtom", - "avatar_template": "https://avatars.discourse.org/v3/letter/l/a9a28c/{size}.png" - }, - { - "username": "dzsat_team", - "name": "Dz Sat Dz Sat", - "avatar_template": "https://avatars.discourse.org/v3/letter/d/eb9ed0/{size}.png" - } - ], - groups: [ - { - "name": "bob", - "usernames": [] - }, - { - "name": "team", - "usernames": [] - } - ] - }); + }, + { + username: "kudos", + name: "Team Blogeto.com", + avatar_template: + "/user_avatar/meta.discourse.org/kudos/{size}/62185_1.png" + }, + { + username: "RosieLinda", + name: "Linda Teaman", + avatar_template: + "https://avatars.discourse.org/v3/letter/r/bc8723/{size}.png" + }, + { + username: "legalatom", + name: "Team LegalAtom", + avatar_template: + "https://avatars.discourse.org/v3/letter/l/a9a28c/{size}.png" + }, + { + username: "dzsat_team", + name: "Dz Sat Dz Sat", + avatar_template: + "https://avatars.discourse.org/v3/letter/d/eb9ed0/{size}.png" + } + ], + groups: [ + { + name: "bob", + usernames: [] + }, + { + name: "team", + usernames: [] + } + ] + }); }); } }); QUnit.test("it places groups unconditionally for exact match", async assert => { - let results = await userSearch({term: 'Team'}); - assert.equal(results[results.length-1]["name"], "team"); + let results = await userSearch({ term: "Team" }); + assert.equal(results[results.length - 1]["name"], "team"); }); diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index 9b5f4e4265..82fbfe6bba 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -16,21 +16,43 @@ import { caretRowCol, setCaretPosition, fillMissingDates -} from 'discourse/lib/utilities'; -import * as Utilities from 'discourse/lib/utilities'; +} from "discourse/lib/utilities"; +import * as Utilities from "discourse/lib/utilities"; QUnit.module("lib:utilities"); QUnit.test("emailValid", assert => { - assert.ok(emailValid('Bob@example.com'), "allows upper case in the first part of emails"); - assert.ok(emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain"); + assert.ok( + emailValid("Bob@example.com"), + "allows upper case in the first part of emails" + ); + assert.ok( + emailValid("bob@EXAMPLE.com"), + "allows upper case in the email domain" + ); }); QUnit.test("extractDomainFromUrl", assert => { - assert.equal(extractDomainFromUrl('http://meta.discourse.org:443/random'), 'meta.discourse.org', "extract domain name from url"); - assert.equal(extractDomainFromUrl('meta.discourse.org:443/random'), 'meta.discourse.org', "extract domain regardless of scheme presence"); - assert.equal(extractDomainFromUrl('http://192.168.0.1:443/random'), '192.168.0.1', "works for IP address"); - assert.equal(extractDomainFromUrl('http://localhost:443/random'), 'localhost', "works for localhost"); + assert.equal( + extractDomainFromUrl("http://meta.discourse.org:443/random"), + "meta.discourse.org", + "extract domain name from url" + ); + assert.equal( + extractDomainFromUrl("meta.discourse.org:443/random"), + "meta.discourse.org", + "extract domain regardless of scheme presence" + ); + assert.equal( + extractDomainFromUrl("http://192.168.0.1:443/random"), + "192.168.0.1", + "works for IP address" + ); + assert.equal( + extractDomainFromUrl("http://localhost:443/random"), + "localhost", + "works for localhost" + ); }); var validUpload = validateUploadedFiles; @@ -45,7 +67,7 @@ QUnit.test("uploading one file", assert => { sandbox.stub(bootbox, "alert"); assert.not(validUpload([1, 2])); - assert.ok(bootbox.alert.calledWith(I18n.t('post.errors.too_many_uploads'))); + assert.ok(bootbox.alert.calledWith(I18n.t("post.errors.too_many_uploads"))); }); QUnit.test("new user cannot upload images", assert => { @@ -53,8 +75,13 @@ QUnit.test("new user cannot upload images", assert => { Discourse.User.resetCurrent(Discourse.User.create()); sandbox.stub(bootbox, "alert"); - assert.not(validUpload([{name: "image.png"}]), 'the upload is not valid'); - assert.ok(bootbox.alert.calledWith(I18n.t('post.errors.image_upload_not_allowed_for_new_user')), 'the alert is called'); + assert.not(validUpload([{ name: "image.png" }]), "the upload is not valid"); + assert.ok( + bootbox.alert.calledWith( + I18n.t("post.errors.image_upload_not_allowed_for_new_user") + ), + "the alert is called" + ); }); QUnit.test("new user cannot upload attachments", assert => { @@ -62,14 +89,24 @@ QUnit.test("new user cannot upload attachments", assert => { sandbox.stub(bootbox, "alert"); Discourse.User.resetCurrent(Discourse.User.create()); - assert.not(validUpload([{name: "roman.txt"}])); - assert.ok(bootbox.alert.calledWith(I18n.t('post.errors.attachment_upload_not_allowed_for_new_user'))); + assert.not(validUpload([{ name: "roman.txt" }])); + assert.ok( + bootbox.alert.calledWith( + I18n.t("post.errors.attachment_upload_not_allowed_for_new_user") + ) + ); }); QUnit.test("ensures an authorized upload", assert => { sandbox.stub(bootbox, "alert"); assert.not(validUpload([{ name: "unauthorized.html" }])); - assert.ok(bootbox.alert.calledWith(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: authorizedExtensions() }))); + assert.ok( + bootbox.alert.calledWith( + I18n.t("post.errors.upload_not_authorized", { + authorized_extensions: authorizedExtensions() + }) + ) + ); }); QUnit.test("staff can upload anything in PM", assert => { @@ -80,19 +117,28 @@ QUnit.test("staff can upload anything in PM", assert => { sandbox.stub(bootbox, "alert"); assert.not(validUpload(files)); - assert.ok(validUpload(files, { isPrivateMessage: true, allowStaffToUploadAnyFileInPm: true })); + assert.ok( + validUpload(files, { + isPrivateMessage: true, + allowStaffToUploadAnyFileInPm: true + }) + ); }); var imageSize = 10 * 1024; var dummyBlob = function() { - var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; + var BlobBuilder = + window.BlobBuilder || + window.WebKitBlobBuilder || + window.MozBlobBuilder || + window.MSBlobBuilder; if (BlobBuilder) { var bb = new BlobBuilder(); bb.append([new Int8Array(imageSize)]); return bb.getBlob("image/png"); } else { - return new Blob([new Int8Array(imageSize)], { "type" : "image\/png" }); + return new Blob([new Int8Array(imageSize)], { type: "image/png" }); } }; @@ -122,23 +168,43 @@ var testUploadMarkdown = function(filename) { }; QUnit.test("getUploadMarkdown", assert => { - assert.equal(testUploadMarkdown("lolcat.gif"),'![lolcat|100x200](/uploads/123/abcdef.ext)'); - assert.equal(testUploadMarkdown("[foo|bar].png"),'![%5Bfoo%7Cbar%5D|100x200](/uploads/123/abcdef.ext)'); - assert.ok(testUploadMarkdown("important.txt") === 'important.txt (42 Bytes)\n'); + assert.equal( + testUploadMarkdown("lolcat.gif"), + "![lolcat|100x200](/uploads/123/abcdef.ext)" + ); + assert.equal( + testUploadMarkdown("[foo|bar].png"), + "![%5Bfoo%7Cbar%5D|100x200](/uploads/123/abcdef.ext)" + ); + assert.ok( + testUploadMarkdown("important.txt") === + 'important.txt (42 Bytes)\n' + ); }); QUnit.test("replaces GUID in image alt text on iOS", assert => { - assert.equal(testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"),'![8F2B469B-6B2C-4213-BC68-57B4876365A0|100x200](/uploads/123/abcdef.ext)'); + assert.equal( + testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"), + "![8F2B469B-6B2C-4213-BC68-57B4876365A0|100x200](/uploads/123/abcdef.ext)" + ); - sandbox.stub(Utilities, 'isAppleDevice').returns(true); - assert.equal(testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"),'![image|100x200](/uploads/123/abcdef.ext)'); + sandbox.stub(Utilities, "isAppleDevice").returns(true); + assert.equal( + testUploadMarkdown("8F2B469B-6B2C-4213-BC68-57B4876365A0.jpeg"), + "![image|100x200](/uploads/123/abcdef.ext)" + ); }); QUnit.test("isAnImage", assert => { - _.each(["png", "jpg", "jpeg", "bmp", "gif", "tif", "tiff", "ico"], function(extension) { + _.each(["png", "jpg", "jpeg", "bmp", "gif", "tif", "tiff", "ico"], function( + extension + ) { var image = "image." + extension; assert.ok(isAnImage(image), image + " is recognized as an image"); - assert.ok(isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image"); + assert.ok( + isAnImage("http://foo.bar/path/to/" + image), + image + " is recognized as an image" + ); }); assert.not(isAnImage("file.txt")); assert.not(isAnImage("http://foo.bar/path/to/file.txt")); @@ -147,13 +213,21 @@ QUnit.test("isAnImage", assert => { QUnit.test("avatarUrl", assert => { var rawSize = getRawSize; - assert.blank(avatarUrl('', 'tiny'), "no template returns blank"); - assert.equal(avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/" + rawSize(20) + ".png", "simple avatar url"); - assert.equal(avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/" + rawSize(45) + ".png", "different size"); + assert.blank(avatarUrl("", "tiny"), "no template returns blank"); + assert.equal( + avatarUrl("/fake/template/{size}.png", "tiny"), + "/fake/template/" + rawSize(20) + ".png", + "simple avatar url" + ); + assert.equal( + avatarUrl("/fake/template/{size}.png", "large"), + "/fake/template/" + rawSize(45) + ".png", + "different size" + ); }); var setDevicePixelRatio = function(value) { - if (Object.defineProperty && !window.hasOwnProperty('devicePixelRatio')) { + if (Object.defineProperty && !window.hasOwnProperty("devicePixelRatio")) { Object.defineProperty(window, "devicePixelRatio", { value: 2 }); } else { window.devicePixelRatio = value; @@ -165,20 +239,36 @@ QUnit.test("avatarImg", assert => { setDevicePixelRatio(2); var avatarTemplate = "/path/to/avatar/{size}.png"; - assert.equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}), - "", - "it returns the avatar html"); + assert.equal( + avatarImg({ avatarTemplate: avatarTemplate, size: "tiny" }), + "", + "it returns the avatar html" + ); - assert.equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}), - "", - "it adds a title if supplied"); + assert.equal( + avatarImg({ + avatarTemplate: avatarTemplate, + size: "tiny", + title: "evilest trout" + }), + "", + "it adds a title if supplied" + ); - assert.equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}), - "", - "it adds extra classes if supplied"); + assert.equal( + avatarImg({ + avatarTemplate: avatarTemplate, + size: "tiny", + extraClasses: "evil fish" + }), + "", + "it adds extra classes if supplied" + ); - assert.blank(avatarImg({avatarTemplate: "", size: 'tiny'}), - "it doesn't render avatars for invalid avatar template"); + assert.blank( + avatarImg({ avatarTemplate: "", size: "tiny" }), + "it doesn't render avatars for invalid avatar template" + ); setDevicePixelRatio(oldRatio); }); @@ -191,22 +281,33 @@ QUnit.test("allowsImages", assert => { assert.ok(allowsImages(), "works with old extensions syntax"); Discourse.SiteSettings.authorized_extensions = "txt|pdf|*"; - assert.ok(allowsImages(), "images are allowed when all extensions are allowed"); + assert.ok( + allowsImages(), + "images are allowed when all extensions are allowed" + ); Discourse.SiteSettings.authorized_extensions = "json|jpg|pdf|txt"; - assert.ok(allowsImages(), "images are allowed when at least one extension is an image extension"); + assert.ok( + allowsImages(), + "images are allowed when at least one extension is an image extension" + ); }); - QUnit.test("allowsAttachments", assert => { Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif"; assert.not(allowsAttachments(), "no attachments allowed by default"); Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|*"; - assert.ok(allowsAttachments(), "attachments are allowed when all extensions are allowed"); + assert.ok( + allowsAttachments(), + "attachments are allowed when all extensions are allowed" + ); Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|pdf"; - assert.ok(allowsAttachments(), "attachments are allowed when at least one extension is not an image extension"); + assert.ok( + allowsAttachments(), + "attachments are allowed when at least one extension is not an image extension" + ); Discourse.SiteSettings.authorized_extensions = ".jpg|.jpeg|.gif|.pdf"; assert.ok(allowsAttachments(), "works with old extensions syntax"); @@ -214,12 +315,20 @@ QUnit.test("allowsAttachments", assert => { QUnit.test("defaultHomepage", assert => { Discourse.SiteSettings.top_menu = "latest|top|hot"; - assert.equal(defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting"); + assert.equal( + defaultHomepage(), + "latest", + "default homepage is the first item in the top_menu site setting" + ); var meta = document.createElement("meta"); meta.name = "discourse_current_homepage"; meta.content = "hot"; document.body.appendChild(meta); - assert.equal(defaultHomepage(), "hot", "default homepage is pulled from "); + assert.equal( + defaultHomepage(), + "hot", + "default homepage is pulled from " + ); document.body.removeChild(meta); }); @@ -229,12 +338,16 @@ QUnit.test("setDefaultHomepage", assert => { meta.content = "hot"; document.body.appendChild(meta); setDefaultHomepage("top"); - assert.equal(meta.content, "top", "default homepage set by setDefaultHomepage"); + assert.equal( + meta.content, + "top", + "default homepage set by setDefaultHomepage" + ); document.body.removeChild(meta); }); QUnit.test("caretRowCol", assert => { - var textarea = document.createElement('textarea'); + var textarea = document.createElement("textarea"); const content = document.createTextNode("01234\n56789\n012345"); textarea.appendChild(content); document.body.appendChild(textarea); @@ -243,8 +356,16 @@ QUnit.test("caretRowCol", assert => { setCaretPosition(textarea, setCaretPos); const result = caretRowCol(textarea); - assert.equal(result.rowNum, expectedRowNum, "returns the right row of the caret"); - assert.equal(result.colNum, expectedColNum, "returns the right col of the caret"); + assert.equal( + result.rowNum, + expectedRowNum, + "returns the right row of the caret" + ); + assert.equal( + result.colNum, + expectedColNum, + "returns the right col of the caret" + ); }; assertResult(0, 1, 0); @@ -259,8 +380,12 @@ QUnit.test("caretRowCol", assert => { QUnit.test("fillMissingDates", assert => { const startDate = "2017-11-12"; // YYYY-MM-DD const endDate = "2017-12-12"; // YYYY-MM-DD - const data = '[{"x":"2017-11-12","y":3},{"x":"2017-11-27","y":2},{"x":"2017-12-06","y":9},{"x":"2017-12-11","y":2}]'; + const data = + '[{"x":"2017-11-12","y":3},{"x":"2017-11-27","y":2},{"x":"2017-12-06","y":9},{"x":"2017-12-11","y":2}]'; - assert.equal(fillMissingDates(JSON.parse(data), startDate, endDate).length, 31, - "it returns a JSON array with 31 dates"); + assert.equal( + fillMissingDates(JSON.parse(data), startDate, endDate).length, + 31, + "it returns a JSON array with 31 dates" + ); }); diff --git a/test/javascripts/lib/white-lister-test.js.es6 b/test/javascripts/lib/white-lister-test.js.es6 index e8bb3d8f30..0529e52515 100644 --- a/test/javascripts/lib/white-lister-test.js.es6 +++ b/test/javascripts/lib/white-lister-test.js.es6 @@ -1,48 +1,58 @@ -import WhiteLister from 'pretty-text/white-lister'; +import WhiteLister from "pretty-text/white-lister"; QUnit.module("lib:whiteLister"); QUnit.test("whiteLister", assert => { const whiteLister = new WhiteLister(); - assert.ok(Object.keys(whiteLister.getWhiteList().tagList).length > 1, "should have some defaults"); + assert.ok( + Object.keys(whiteLister.getWhiteList().tagList).length > 1, + "should have some defaults" + ); whiteLister.disable("default"); - assert.ok(Object.keys(whiteLister.getWhiteList().tagList).length === 0, "should have no defaults if disabled"); + assert.ok( + Object.keys(whiteLister.getWhiteList().tagList).length === 0, + "should have no defaults if disabled" + ); whiteLister.whiteListFeature("test", [ - 'custom.foo', - 'custom.baz', - 'custom[data-*]', - 'custom[rel=nofollow]' + "custom.foo", + "custom.baz", + "custom[data-*]", + "custom[rel=nofollow]" ]); - whiteLister.whiteListFeature("test", [ - 'custom[rel=test]' - ]); + whiteLister.whiteListFeature("test", ["custom[rel=test]"]); whiteLister.enable("test"); - assert.deepEqual(whiteLister.getWhiteList(), { - tagList: { - custom: [] - }, - attrList: { - custom: { - "class": ["foo", "baz"], - "data-*": ["*"], - "rel": ["nofollow", "test"] + assert.deepEqual( + whiteLister.getWhiteList(), + { + tagList: { + custom: [] + }, + attrList: { + custom: { + class: ["foo", "baz"], + "data-*": ["*"], + rel: ["nofollow", "test"] + } } - } - }, 'Expecting a correct white list'); - + }, + "Expecting a correct white list" + ); whiteLister.disable("test"); - assert.deepEqual(whiteLister.getWhiteList(), { - tagList: {}, - attrList: {} - }, 'Expecting an empty white list'); - + assert.deepEqual( + whiteLister.getWhiteList(), + { + tagList: {}, + attrList: {} + }, + "Expecting an empty white list" + ); }); diff --git a/test/javascripts/mixins/grant-badge-controller-test.js.es6 b/test/javascripts/mixins/grant-badge-controller-test.js.es6 index d1e34cd133..c08276c80d 100644 --- a/test/javascripts/mixins/grant-badge-controller-test.js.es6 +++ b/test/javascripts/mixins/grant-badge-controller-test.js.es6 @@ -1,38 +1,83 @@ -import GrantBadgeControllerMixin from 'discourse/mixins/grant-badge-controller'; -import Badge from 'discourse/models/badge'; +import GrantBadgeControllerMixin from "discourse/mixins/grant-badge-controller"; +import Badge from "discourse/models/badge"; -QUnit.module('mixin:grant-badge-controller', { +QUnit.module("mixin:grant-badge-controller", { before: function() { - this.GrantBadgeController = Ember.Controller.extend(GrantBadgeControllerMixin); + this.GrantBadgeController = Ember.Controller.extend( + GrantBadgeControllerMixin + ); - this.badgeFirst = Badge.create({ id: 3, name: 'A Badge', enabled: true, manually_grantable: true }); - this.badgeMiddle = Badge.create({ id: 1, name: 'My Badge', enabled: true, manually_grantable: true }); - this.badgeLast = Badge.create({ id: 2, name: 'Zoo Badge', enabled: true, manually_grantable: true }); - this.badgeDisabled = Badge.create({ id: 4, name: 'Disabled Badge', enabled: false, manually_grantable: true }); - this.badgeAutomatic = Badge.create({ id: 5, name: 'Automatic Badge', enabled: true, manually_grantable: false }); + this.badgeFirst = Badge.create({ + id: 3, + name: "A Badge", + enabled: true, + manually_grantable: true + }); + this.badgeMiddle = Badge.create({ + id: 1, + name: "My Badge", + enabled: true, + manually_grantable: true + }); + this.badgeLast = Badge.create({ + id: 2, + name: "Zoo Badge", + enabled: true, + manually_grantable: true + }); + this.badgeDisabled = Badge.create({ + id: 4, + name: "Disabled Badge", + enabled: false, + manually_grantable: true + }); + this.badgeAutomatic = Badge.create({ + id: 5, + name: "Automatic Badge", + enabled: true, + manually_grantable: false + }); }, beforeEach: function() { this.subject = this.GrantBadgeController.create({ userBadges: [], - allBadges: [this.badgeLast, this.badgeFirst, this.badgeMiddle, this.badgeDisabled, this.badgeAutomatic], + allBadges: [ + this.badgeLast, + this.badgeFirst, + this.badgeMiddle, + this.badgeDisabled, + this.badgeAutomatic + ] }); } }); -QUnit.test('grantableBadges', function(assert) { - const sortedNames = [this.badgeFirst.name, this.badgeMiddle.name, this.badgeLast.name]; - const badgeNames = this.subject.get('grantableBadges').map(badge => badge.name); +QUnit.test("grantableBadges", function(assert) { + const sortedNames = [ + this.badgeFirst.name, + this.badgeMiddle.name, + this.badgeLast.name + ]; + const badgeNames = this.subject + .get("grantableBadges") + .map(badge => badge.name); - assert.not(badgeNames.includes(this.badgeDisabled), 'excludes disabled badges'); - assert.not(badgeNames.includes(this.badgeAutomatic), 'excludes automatic badges'); - assert.deepEqual(badgeNames, sortedNames, 'sorts badges by name'); + assert.not( + badgeNames.includes(this.badgeDisabled), + "excludes disabled badges" + ); + assert.not( + badgeNames.includes(this.badgeAutomatic), + "excludes automatic badges" + ); + assert.deepEqual(badgeNames, sortedNames, "sorts badges by name"); }); -QUnit.test('selectedBadgeGrantable', function(assert) { - this.subject.set('selectedBadgeId', this.badgeDisabled.id); - assert.not(this.subject.get('selectedBadgeGrantable')); +QUnit.test("selectedBadgeGrantable", function(assert) { + this.subject.set("selectedBadgeId", this.badgeDisabled.id); + assert.not(this.subject.get("selectedBadgeGrantable")); - this.subject.set('selectedBadgeId', this.badgeFirst.id); - assert.ok(this.subject.get('selectedBadgeGrantable')); + this.subject.set("selectedBadgeId", this.badgeFirst.id); + assert.ok(this.subject.get("selectedBadgeGrantable")); }); diff --git a/test/javascripts/mixins/singleton-test.js.es6 b/test/javascripts/mixins/singleton-test.js.es6 index 2e5635a4d1..0ed6c198ee 100644 --- a/test/javascripts/mixins/singleton-test.js.es6 +++ b/test/javascripts/mixins/singleton-test.js.es6 @@ -1,4 +1,4 @@ -import Singleton from 'discourse/mixins/singleton'; +import Singleton from "discourse/mixins/singleton"; QUnit.module("mixin:singleton"); @@ -7,9 +7,17 @@ QUnit.test("current", assert => { DummyModel.reopenClass(Singleton); var current = DummyModel.current(); - assert.present(current, 'current returns the current instance'); - assert.equal(current, DummyModel.current(), 'calling it again returns the same instance'); - assert.notEqual(current, DummyModel.create({}), 'we can create other instances that are not the same as current'); + assert.present(current, "current returns the current instance"); + assert.equal( + current, + DummyModel.current(), + "calling it again returns the same instance" + ); + assert.notEqual( + current, + DummyModel.create({}), + "we can create other instances that are not the same as current" + ); }); QUnit.test("currentProp reading", assert => { @@ -17,39 +25,60 @@ QUnit.test("currentProp reading", assert => { DummyModel.reopenClass(Singleton); var current = DummyModel.current(); - assert.blank(DummyModel.currentProp('evil'), 'by default attributes are blank'); - current.set('evil', 'trout'); - assert.equal(DummyModel.currentProp('evil'), 'trout', 'after changing the instance, the value is set'); + assert.blank( + DummyModel.currentProp("evil"), + "by default attributes are blank" + ); + current.set("evil", "trout"); + assert.equal( + DummyModel.currentProp("evil"), + "trout", + "after changing the instance, the value is set" + ); }); QUnit.test("currentProp writing", assert => { var DummyModel = Ember.Object.extend({}); DummyModel.reopenClass(Singleton); - assert.blank(DummyModel.currentProp('adventure'), 'by default attributes are blank'); - var result = DummyModel.currentProp('adventure', 'time'); - assert.equal(result, 'time', 'it returns the new value'); - assert.equal(DummyModel.currentProp('adventure'), 'time', 'after calling currentProp the value is set'); + assert.blank( + DummyModel.currentProp("adventure"), + "by default attributes are blank" + ); + var result = DummyModel.currentProp("adventure", "time"); + assert.equal(result, "time", "it returns the new value"); + assert.equal( + DummyModel.currentProp("adventure"), + "time", + "after calling currentProp the value is set" + ); - DummyModel.currentProp('count', 0); - assert.equal(DummyModel.currentProp('count'), 0, 'we can set the value to 0'); + DummyModel.currentProp("count", 0); + assert.equal(DummyModel.currentProp("count"), 0, "we can set the value to 0"); - DummyModel.currentProp('adventure', null); - assert.equal(DummyModel.currentProp('adventure'), null, 'we can set the value to null'); + DummyModel.currentProp("adventure", null); + assert.equal( + DummyModel.currentProp("adventure"), + null, + "we can set the value to null" + ); }); QUnit.test("createCurrent", assert => { var Shoe = Ember.Object.extend({}); Shoe.reopenClass(Singleton, { createCurrent: function() { - return Shoe.create({toes: 5}); + return Shoe.create({ toes: 5 }); } }); - assert.equal(Shoe.currentProp('toes'), 5, 'it created the class using `createCurrent`'); + assert.equal( + Shoe.currentProp("toes"), + 5, + "it created the class using `createCurrent`" + ); }); - QUnit.test("createCurrent that returns null", assert => { var Missing = Ember.Object.extend({}); Missing.reopenClass(Singleton, { @@ -59,5 +88,8 @@ QUnit.test("createCurrent that returns null", assert => { }); assert.blank(Missing.current(), "it doesn't return an instance"); - assert.blank(Missing.currentProp('madeup'), "it won't raise an error asking for a property. Will just return null."); -}); \ No newline at end of file + assert.blank( + Missing.currentProp("madeup"), + "it won't raise an error asking for a property. Will just return null." + ); +}); diff --git a/test/javascripts/models/badge-test.js.es6 b/test/javascripts/models/badge-test.js.es6 index d9a6e0b2d3..e29710e1f8 100644 --- a/test/javascripts/models/badge-test.js.es6 +++ b/test/javascripts/models/badge-test.js.es6 @@ -1,51 +1,75 @@ -import Badge from 'discourse/models/badge'; +import Badge from "discourse/models/badge"; QUnit.module("model:badge"); -QUnit.test('newBadge', assert => { - const badge1 = Badge.create({name: "New Badge"}), - badge2 = Badge.create({id: 1, name: "Old Badge"}); - assert.ok(badge1.get('newBadge'), "badges without ids are new"); - assert.ok(!badge2.get('newBadge'), "badges with ids are not new"); +QUnit.test("newBadge", assert => { + const badge1 = Badge.create({ name: "New Badge" }), + badge2 = Badge.create({ id: 1, name: "Old Badge" }); + assert.ok(badge1.get("newBadge"), "badges without ids are new"); + assert.ok(!badge2.get("newBadge"), "badges with ids are not new"); }); - -QUnit.test('createFromJson array', assert => { - const badgesJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badges":[{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}]}; +QUnit.test("createFromJson array", assert => { + const badgesJson = { + badge_types: [{ id: 6, name: "Silver 1" }], + badges: [{ id: 1126, name: "Badge 1", description: null, badge_type_id: 6 }] + }; const badges = Badge.createFromJson(badgesJson); assert.ok(Array.isArray(badges), "returns an array"); - assert.equal(badges[0].get('name'), "Badge 1", "badge details are set"); - assert.equal(badges[0].get('badge_type.name'), "Silver 1", "badge_type reference is set"); + assert.equal(badges[0].get("name"), "Badge 1", "badge details are set"); + assert.equal( + badges[0].get("badge_type.name"), + "Silver 1", + "badge_type reference is set" + ); }); -QUnit.test('createFromJson single', assert => { - const badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}}; +QUnit.test("createFromJson single", assert => { + const badgeJson = { + badge_types: [{ id: 6, name: "Silver 1" }], + badge: { id: 1126, name: "Badge 1", description: null, badge_type_id: 6 } + }; const badge = Badge.createFromJson(badgeJson); assert.ok(!Array.isArray(badge), "does not returns an array"); }); -QUnit.test('updateFromJson', assert => { - const badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}}; - const badge = Badge.create({name: "Badge 1"}); +QUnit.test("updateFromJson", assert => { + const badgeJson = { + badge_types: [{ id: 6, name: "Silver 1" }], + badge: { id: 1126, name: "Badge 1", description: null, badge_type_id: 6 } + }; + const badge = Badge.create({ name: "Badge 1" }); badge.updateFromJson(badgeJson); - assert.equal(badge.get('id'), 1126, "id is set"); - assert.equal(badge.get('badge_type.name'), "Silver 1", "badge_type reference is set"); + assert.equal(badge.get("id"), 1126, "id is set"); + assert.equal( + badge.get("badge_type.name"), + "Silver 1", + "badge_type reference is set" + ); }); -QUnit.test('save', assert => { +QUnit.test("save", assert => { assert.expect(0); - const badge = Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1}); + const badge = Badge.create({ + name: "New Badge", + description: "This is a new badge.", + badge_type_id: 1 + }); return badge.save(["name", "description", "badge_type_id"]); }); -QUnit.test('destroy', assert => { +QUnit.test("destroy", assert => { assert.expect(0); - const badge = Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1}); + const badge = Badge.create({ + name: "New Badge", + description: "This is a new badge.", + badge_type_id: 1 + }); badge.destroy(); - badge.set('id', 3); + badge.set("id", 3); return badge.destroy(); -}); \ No newline at end of file +}); diff --git a/test/javascripts/models/category-test.js.es6 b/test/javascripts/models/category-test.js.es6 index fc5065566a..9e2437cf57 100644 --- a/test/javascripts/models/category-test.js.es6 +++ b/test/javascripts/models/category-test.js.es6 @@ -1,146 +1,322 @@ -import createStore from 'helpers/create-store'; -import Category from 'discourse/models/category'; +import createStore from "helpers/create-store"; +import Category from "discourse/models/category"; QUnit.module("model:category"); -QUnit.test('slugFor', assert =>{ +QUnit.test("slugFor", assert => { const store = createStore(); const slugFor = function(cat, val, text) { assert.equal(Discourse.Category.slugFor(cat), val, text); }; - slugFor(store.createRecord('category', {slug: 'hello'}), "hello", "It calculates the proper slug for hello"); - slugFor(store.createRecord('category', {id: 123, slug: ''}), "123-category", "It returns id-category for empty strings"); - slugFor(store.createRecord('category', {id: 456}), "456-category", "It returns id-category for undefined slugs"); - slugFor(store.createRecord('category', {slug: '熱帶風暴畫眉'}), "熱帶風暴畫眉", "It can be non english characters"); + slugFor( + store.createRecord("category", { slug: "hello" }), + "hello", + "It calculates the proper slug for hello" + ); + slugFor( + store.createRecord("category", { id: 123, slug: "" }), + "123-category", + "It returns id-category for empty strings" + ); + slugFor( + store.createRecord("category", { id: 456 }), + "456-category", + "It returns id-category for undefined slugs" + ); + slugFor( + store.createRecord("category", { slug: "熱帶風暴畫眉" }), + "熱帶風暴畫眉", + "It can be non english characters" + ); - const parentCategory = store.createRecord('category', {id: 345, slug: 'darth'}); - slugFor(store.createRecord('category', {slug: 'luke', parentCategory: parentCategory}), - "darth/luke", - "it uses the parent slug before the child"); + const parentCategory = store.createRecord("category", { + id: 345, + slug: "darth" + }); + slugFor( + store.createRecord("category", { + slug: "luke", + parentCategory: parentCategory + }), + "darth/luke", + "it uses the parent slug before the child" + ); - slugFor(store.createRecord('category', {id: 555, parentCategory: parentCategory}), - "darth/555-category", - "it uses the parent slug before the child and then uses id"); + slugFor( + store.createRecord("category", { id: 555, parentCategory: parentCategory }), + "darth/555-category", + "it uses the parent slug before the child and then uses id" + ); - parentCategory.set('slug', null); - slugFor(store.createRecord('category', {id: 555, parentCategory: parentCategory}), - "345-category/555-category", - "it uses the parent before the child and uses ids for both"); + parentCategory.set("slug", null); + slugFor( + store.createRecord("category", { id: 555, parentCategory: parentCategory }), + "345-category/555-category", + "it uses the parent before the child and uses ids for both" + ); }); - -QUnit.test('findBySlug', assert => { +QUnit.test("findBySlug", assert => { assert.expect(6); const store = createStore(); - const darth = store.createRecord('category', {id: 1, slug: 'darth'}), - luke = store.createRecord('category', {id: 2, slug: 'luke', parentCategory: darth}), - hurricane = store.createRecord('category', {id: 3, slug: '熱帶風暴畫眉'}), - newsFeed = store.createRecord('category', {id: 4, slug: '뉴스피드', parentCategory: hurricane}), - time = store.createRecord('category', {id: 5, slug: '时间', parentCategory: darth}), - bah = store.createRecord('category', {id: 6, slug: 'bah', parentCategory: hurricane}), + const darth = store.createRecord("category", { id: 1, slug: "darth" }), + luke = store.createRecord("category", { + id: 2, + slug: "luke", + parentCategory: darth + }), + hurricane = store.createRecord("category", { id: 3, slug: "熱帶風暴畫眉" }), + newsFeed = store.createRecord("category", { + id: 4, + slug: "뉴스피드", + parentCategory: hurricane + }), + time = store.createRecord("category", { + id: 5, + slug: "时间", + parentCategory: darth + }), + bah = store.createRecord("category", { + id: 6, + slug: "bah", + parentCategory: hurricane + }), categoryList = [darth, luke, hurricane, newsFeed, time, bah]; - sandbox.stub(Discourse.Category, 'list').returns(categoryList); + sandbox.stub(Discourse.Category, "list").returns(categoryList); - assert.deepEqual(Discourse.Category.findBySlug('darth'), darth, 'we can find a category'); - assert.deepEqual(Discourse.Category.findBySlug('luke', 'darth'), luke, 'we can find the other category with parent category'); - assert.deepEqual(Discourse.Category.findBySlug('熱帶風暴畫眉'), hurricane, 'we can find a category with CJK slug'); - assert.deepEqual(Discourse.Category.findBySlug('뉴스피드', '熱帶風暴畫眉'), newsFeed, 'we can find a category with CJK slug whose parent slug is also CJK'); - assert.deepEqual(Discourse.Category.findBySlug('时间', 'darth'), time, 'we can find a category with CJK slug whose parent slug is english'); - assert.deepEqual(Discourse.Category.findBySlug('bah', '熱帶風暴畫眉'), bah, 'we can find a category with english slug whose parent slug is CJK'); + assert.deepEqual( + Discourse.Category.findBySlug("darth"), + darth, + "we can find a category" + ); + assert.deepEqual( + Discourse.Category.findBySlug("luke", "darth"), + luke, + "we can find the other category with parent category" + ); + assert.deepEqual( + Discourse.Category.findBySlug("熱帶風暴畫眉"), + hurricane, + "we can find a category with CJK slug" + ); + assert.deepEqual( + Discourse.Category.findBySlug("뉴스피드", "熱帶風暴畫眉"), + newsFeed, + "we can find a category with CJK slug whose parent slug is also CJK" + ); + assert.deepEqual( + Discourse.Category.findBySlug("时间", "darth"), + time, + "we can find a category with CJK slug whose parent slug is english" + ); + assert.deepEqual( + Discourse.Category.findBySlug("bah", "熱帶風暴畫眉"), + bah, + "we can find a category with english slug whose parent slug is CJK" + ); sandbox.restore(); }); -QUnit.test('findSingleBySlug', assert => { +QUnit.test("findSingleBySlug", assert => { assert.expect(6); const store = createStore(); - const darth = store.createRecord('category', {id: 1, slug: 'darth'}), - luke = store.createRecord('category', {id: 2, slug: 'luke', parentCategory: darth}), - hurricane = store.createRecord('category', {id: 3, slug: '熱帶風暴畫眉'}), - newsFeed = store.createRecord('category', {id: 4, slug: '뉴스피드', parentCategory: hurricane}), - time = store.createRecord('category', {id: 5, slug: '时间', parentCategory: darth}), - bah = store.createRecord('category', {id: 6, slug: 'bah', parentCategory: hurricane}), + const darth = store.createRecord("category", { id: 1, slug: "darth" }), + luke = store.createRecord("category", { + id: 2, + slug: "luke", + parentCategory: darth + }), + hurricane = store.createRecord("category", { id: 3, slug: "熱帶風暴畫眉" }), + newsFeed = store.createRecord("category", { + id: 4, + slug: "뉴스피드", + parentCategory: hurricane + }), + time = store.createRecord("category", { + id: 5, + slug: "时间", + parentCategory: darth + }), + bah = store.createRecord("category", { + id: 6, + slug: "bah", + parentCategory: hurricane + }), categoryList = [darth, luke, hurricane, newsFeed, time, bah]; - sandbox.stub(Discourse.Category, 'list').returns(categoryList); + sandbox.stub(Discourse.Category, "list").returns(categoryList); - assert.deepEqual(Discourse.Category.findSingleBySlug('darth'), darth, 'we can find a category'); - assert.deepEqual(Discourse.Category.findSingleBySlug('darth/luke'), luke, 'we can find the other category with parent category'); - assert.deepEqual(Discourse.Category.findSingleBySlug('熱帶風暴畫眉'), hurricane, 'we can find a category with CJK slug'); - assert.deepEqual(Discourse.Category.findSingleBySlug('熱帶風暴畫眉/뉴스피드'), newsFeed, 'we can find a category with CJK slug whose parent slug is also CJK'); - assert.deepEqual(Discourse.Category.findSingleBySlug('darth/时间'), time, 'we can find a category with CJK slug whose parent slug is english'); - assert.deepEqual(Discourse.Category.findSingleBySlug('熱帶風暴畫眉/bah'), bah, 'we can find a category with english slug whose parent slug is CJK'); + assert.deepEqual( + Discourse.Category.findSingleBySlug("darth"), + darth, + "we can find a category" + ); + assert.deepEqual( + Discourse.Category.findSingleBySlug("darth/luke"), + luke, + "we can find the other category with parent category" + ); + assert.deepEqual( + Discourse.Category.findSingleBySlug("熱帶風暴畫眉"), + hurricane, + "we can find a category with CJK slug" + ); + assert.deepEqual( + Discourse.Category.findSingleBySlug("熱帶風暴畫眉/뉴스피드"), + newsFeed, + "we can find a category with CJK slug whose parent slug is also CJK" + ); + assert.deepEqual( + Discourse.Category.findSingleBySlug("darth/时间"), + time, + "we can find a category with CJK slug whose parent slug is english" + ); + assert.deepEqual( + Discourse.Category.findSingleBySlug("熱帶風暴畫眉/bah"), + bah, + "we can find a category with english slug whose parent slug is CJK" + ); }); -QUnit.test('findByIds', assert => { +QUnit.test("findByIds", assert => { const store = createStore(); - const categories = { - 1: store.createRecord('category', {id: 1}), - 2: store.createRecord('category', {id: 2}) + const categories = { + 1: store.createRecord("category", { id: 1 }), + 2: store.createRecord("category", { id: 2 }) }; - sandbox.stub(Discourse.Category, 'idMap').returns(categories); - assert.deepEqual(Discourse.Category.findByIds([1,2,3]), _.values(categories)); + sandbox.stub(Discourse.Category, "idMap").returns(categories); + assert.deepEqual( + Discourse.Category.findByIds([1, 2, 3]), + _.values(categories) + ); }); -QUnit.test('search with category name', assert => { +QUnit.test("search with category name", assert => { const store = createStore(), - category1 = store.createRecord('category', { id: 1, name: 'middle term', slug: 'different-slug' }), - category2 = store.createRecord('category', { id: 2, name: 'middle term', slug: 'another-different-slug' }); + category1 = store.createRecord("category", { + id: 1, + name: "middle term", + slug: "different-slug" + }), + category2 = store.createRecord("category", { + id: 2, + name: "middle term", + slug: "another-different-slug" + }); sandbox.stub(Category, "listByActivity").returns([category1, category2]); - assert.deepEqual(Category.search('term', { limit: 0 }), [], "returns an empty array when limit is 0"); - assert.deepEqual(Category.search(''), [category1, category2], "orders by activity if no term is matched"); - assert.deepEqual(Category.search('term'), [category1, category2], "orders by activity"); + assert.deepEqual( + Category.search("term", { limit: 0 }), + [], + "returns an empty array when limit is 0" + ); + assert.deepEqual( + Category.search(""), + [category1, category2], + "orders by activity if no term is matched" + ); + assert.deepEqual( + Category.search("term"), + [category1, category2], + "orders by activity" + ); - category2.set('name', 'TeRm start'); - assert.deepEqual(Category.search('tErM'), [category2, category1], "ignores case of category name and search term"); + category2.set("name", "TeRm start"); + assert.deepEqual( + Category.search("tErM"), + [category2, category1], + "ignores case of category name and search term" + ); - category2.set('name', 'term start'); - assert.deepEqual(Category.search('term'), [category2, category1], "orders matching begin with and then contains"); + category2.set("name", "term start"); + assert.deepEqual( + Category.search("term"), + [category2, category1], + "orders matching begin with and then contains" + ); sandbox.restore(); - const child_category1 = store.createRecord('category', { id: 3, name: 'term start', parent_category_id: category1.get('id') }), - read_restricted_category = store.createRecord('category', { id: 4, name: 'some term', read_restricted: true }); + const child_category1 = store.createRecord("category", { + id: 3, + name: "term start", + parent_category_id: category1.get("id") + }), + read_restricted_category = store.createRecord("category", { + id: 4, + name: "some term", + read_restricted: true + }); - sandbox.stub(Category, "listByActivity").returns([read_restricted_category, category1, child_category1, category2]); + sandbox + .stub(Category, "listByActivity") + .returns([read_restricted_category, category1, child_category1, category2]); - assert.deepEqual(Category.search(''), - [category1, category2, read_restricted_category], - "prioritize non read_restricted and does not include child categories when term is blank"); + assert.deepEqual( + Category.search(""), + [category1, category2, read_restricted_category], + "prioritize non read_restricted and does not include child categories when term is blank" + ); - assert.deepEqual(Category.search('', { limit: 3 }), - [category1, category2, read_restricted_category], - "prioritize non read_restricted and does not include child categories categories when term is blank with limit"); + assert.deepEqual( + Category.search("", { limit: 3 }), + [category1, category2, read_restricted_category], + "prioritize non read_restricted and does not include child categories categories when term is blank with limit" + ); - assert.deepEqual(Category.search('term'), - [child_category1, category2, category1, read_restricted_category], - "prioritize non read_restricted"); + assert.deepEqual( + Category.search("term"), + [child_category1, category2, category1, read_restricted_category], + "prioritize non read_restricted" + ); - assert.deepEqual(Category.search('term', { limit: 3 }), - [child_category1, category2, read_restricted_category], - "prioritize non read_restricted with limit"); + assert.deepEqual( + Category.search("term", { limit: 3 }), + [child_category1, category2, read_restricted_category], + "prioritize non read_restricted with limit" + ); sandbox.restore(); }); -QUnit.test('search with category slug', assert => { +QUnit.test("search with category slug", assert => { const store = createStore(), - category1 = store.createRecord('category', { id: 1, name: 'middle term', slug: 'different-slug' }), - category2 = store.createRecord('category', { id: 2, name: 'middle term', slug: 'another-different-slug' }); + category1 = store.createRecord("category", { + id: 1, + name: "middle term", + slug: "different-slug" + }), + category2 = store.createRecord("category", { + id: 2, + name: "middle term", + slug: "another-different-slug" + }); sandbox.stub(Category, "listByActivity").returns([category1, category2]); - assert.deepEqual(Category.search('different-slug'), [category1, category2], "returns the right categories"); - assert.deepEqual(Category.search('another-different'), [category2], "returns the right categories"); + assert.deepEqual( + Category.search("different-slug"), + [category1, category2], + "returns the right categories" + ); + assert.deepEqual( + Category.search("another-different"), + [category2], + "returns the right categories" + ); - category2.set('slug', 'ANOTher-DIFfereNT'); - assert.deepEqual(Category.search('anOtHer-dIfFeREnt'), [category2], "ignores case of category slug and search term"); + category2.set("slug", "ANOTher-DIFfereNT"); + assert.deepEqual( + Category.search("anOtHer-dIfFeREnt"), + [category2], + "ignores case of category slug and search term" + ); }); diff --git a/test/javascripts/models/composer-test.js.es6 b/test/javascripts/models/composer-test.js.es6 index 4add3a8452..f1706421f7 100644 --- a/test/javascripts/models/composer-test.js.es6 +++ b/test/javascripts/models/composer-test.js.es6 @@ -1,13 +1,13 @@ -import { currentUser } from 'helpers/qunit-helpers'; -import Composer from 'discourse/models/composer'; -import createStore from 'helpers/create-store'; +import { currentUser } from "helpers/qunit-helpers"; +import Composer from "discourse/models/composer"; +import createStore from "helpers/create-store"; QUnit.module("model:composer"); function createComposer(opts) { opts = opts || {}; opts.user = opts.user || currentUser(); - return createStore().createRecord('composer', opts); + return createStore().createRecord("composer", opts); } function openComposer(opts) { @@ -16,85 +16,150 @@ function openComposer(opts) { return composer; } -QUnit.test('replyLength', assert => { +QUnit.test("replyLength", assert => { const replyLength = function(val, expectedLength) { const composer = createComposer({ reply: val }); - assert.equal(composer.get('replyLength'), expectedLength); + assert.equal(composer.get("replyLength"), expectedLength); }; replyLength("basic reply", 11, "basic reply length"); replyLength(" \nbasic reply\t", 11, "trims whitespaces"); replyLength("ba sic\n\nreply", 12, "count only significant whitespaces"); - replyLength("1[quote=]not counted[/quote]2[quote=]at all[/quote]3", 3, "removes quotes"); - replyLength("1[quote=]not[quote=]counted[/quote]yay[/quote]2", 2, "handles nested quotes correctly"); + replyLength( + "1[quote=]not counted[/quote]2[quote=]at all[/quote]3", + 3, + "removes quotes" + ); + replyLength( + "1[quote=]not[quote=]counted[/quote]yay[/quote]2", + 2, + "handles nested quotes correctly" + ); }); -QUnit.test('missingReplyCharacters', assert => { +QUnit.test("missingReplyCharacters", assert => { Discourse.SiteSettings.min_first_post_length = 40; - const missingReplyCharacters = function(val, isPM, isFirstPost, expected, message) { - const composer = createComposer({ reply: val, creatingPrivateMessage: isPM, creatingTopic: isFirstPost }); - assert.equal(composer.get('missingReplyCharacters'), expected, message); + const missingReplyCharacters = function( + val, + isPM, + isFirstPost, + expected, + message + ) { + const composer = createComposer({ + reply: val, + creatingPrivateMessage: isPM, + creatingTopic: isFirstPost + }); + assert.equal(composer.get("missingReplyCharacters"), expected, message); }; - missingReplyCharacters('hi', false, false, Discourse.SiteSettings.min_post_length - 2, 'too short public post'); - missingReplyCharacters('hi', false, true, Discourse.SiteSettings.min_first_post_length - 2, 'too short first post'); - missingReplyCharacters('hi', true, false, Discourse.SiteSettings.min_personal_message_post_length - 2, 'too short private message'); + missingReplyCharacters( + "hi", + false, + false, + Discourse.SiteSettings.min_post_length - 2, + "too short public post" + ); + missingReplyCharacters( + "hi", + false, + true, + Discourse.SiteSettings.min_first_post_length - 2, + "too short first post" + ); + missingReplyCharacters( + "hi", + true, + false, + Discourse.SiteSettings.min_personal_message_post_length - 2, + "too short private message" + ); const link = "http://imgur.com/gallery/grxX8"; - const composer = createComposer({ canEditTopicFeaturedLink: true, title: link, featuredLink: link, reply: link }); + const composer = createComposer({ + canEditTopicFeaturedLink: true, + title: link, + featuredLink: link, + reply: link + }); - assert.equal(composer.get('missingReplyCharacters'), 0, "don't require any post content"); + assert.equal( + composer.get("missingReplyCharacters"), + 0, + "don't require any post content" + ); }); -QUnit.test('missingTitleCharacters', assert => { +QUnit.test("missingTitleCharacters", assert => { const missingTitleCharacters = function(val, isPM, expected, message) { - const composer = createComposer({ title: val, creatingPrivateMessage: isPM }); - assert.equal(composer.get('missingTitleCharacters'), expected, message); + const composer = createComposer({ + title: val, + creatingPrivateMessage: isPM + }); + assert.equal(composer.get("missingTitleCharacters"), expected, message); }; - missingTitleCharacters('hi', false, Discourse.SiteSettings.min_topic_title_length - 2, 'too short post title'); - missingTitleCharacters('z', true, Discourse.SiteSettings.min_personal_message_title_length - 1, 'too short pm title'); + missingTitleCharacters( + "hi", + false, + Discourse.SiteSettings.min_topic_title_length - 2, + "too short post title" + ); + missingTitleCharacters( + "z", + true, + Discourse.SiteSettings.min_personal_message_title_length - 1, + "too short pm title" + ); }); -QUnit.test('replyDirty', assert => { +QUnit.test("replyDirty", assert => { const composer = createComposer(); - assert.ok(!composer.get('replyDirty'), "by default it's false"); + assert.ok(!composer.get("replyDirty"), "by default it's false"); composer.setProperties({ originalText: "hello", reply: "hello" }); - assert.ok(!composer.get('replyDirty'), "it's false when the originalText is the same as the reply"); - composer.set('reply', 'hello world'); - assert.ok(composer.get('replyDirty'), "it's true when the reply changes"); + assert.ok( + !composer.get("replyDirty"), + "it's false when the originalText is the same as the reply" + ); + composer.set("reply", "hello world"); + assert.ok(composer.get("replyDirty"), "it's true when the reply changes"); }); QUnit.test("appendText", assert => { const composer = createComposer(); - assert.blank(composer.get('reply'), "the reply is blank by default"); + assert.blank(composer.get("reply"), "the reply is blank by default"); composer.appendText("hello"); - assert.equal(composer.get('reply'), "hello", "it appends text to nothing"); + assert.equal(composer.get("reply"), "hello", "it appends text to nothing"); composer.appendText(" world"); - assert.equal(composer.get('reply'), "hello world", "it appends text to existing text"); + assert.equal( + composer.get("reply"), + "hello world", + "it appends text to existing text" + ); composer.clearState(); composer.appendText("a\n\n\n\nb"); - composer.appendText("c",3,{block: true}); + composer.appendText("c", 3, { block: true }); assert.equal(composer.get("reply"), "a\n\nc\n\nb"); composer.clearState(); composer.appendText("ab"); - composer.appendText("c",1,{block: true}); + composer.appendText("c", 1, { block: true }); assert.equal(composer.get("reply"), "a\n\nc\n\nb"); composer.clearState(); composer.appendText("\nab"); - composer.appendText("c",0,{block: true}); + composer.appendText("c", 0, { block: true }); assert.equal(composer.get("reply"), "c\n\nab"); }); @@ -102,16 +167,24 @@ QUnit.test("appendText", assert => { QUnit.test("prependText", assert => { const composer = createComposer(); - assert.blank(composer.get('reply'), "the reply is blank by default"); + assert.blank(composer.get("reply"), "the reply is blank by default"); composer.prependText("hello"); - assert.equal(composer.get('reply'), "hello", "it prepends text to nothing"); + assert.equal(composer.get("reply"), "hello", "it prepends text to nothing"); composer.prependText("world "); - assert.equal(composer.get('reply'), "world hello", "it prepends text to existing text"); + assert.equal( + composer.get("reply"), + "world hello", + "it prepends text to existing text" + ); - composer.prependText("before new line", {new_line: true}); - assert.equal(composer.get('reply'), "before new line\n\nworld hello", "it prepends text with new line to existing text"); + composer.prependText("before new line", { new_line: true }); + assert.equal( + composer.get("reply"), + "before new line\n\nworld hello", + "it prepends text with new line to existing text" + ); }); QUnit.test("Title length for regular topics", assert => { @@ -119,44 +192,44 @@ QUnit.test("Title length for regular topics", assert => { Discourse.SiteSettings.max_topic_title_length = 10; const composer = createComposer(); - composer.set('title', 'asdf'); - assert.ok(!composer.get('titleLengthValid'), "short titles are not valid"); + composer.set("title", "asdf"); + assert.ok(!composer.get("titleLengthValid"), "short titles are not valid"); - composer.set('title', 'this is a long title'); - assert.ok(!composer.get('titleLengthValid'), "long titles are not valid"); + composer.set("title", "this is a long title"); + assert.ok(!composer.get("titleLengthValid"), "long titles are not valid"); - composer.set('title', 'just right'); - assert.ok(composer.get('titleLengthValid'), "in the range is okay"); + composer.set("title", "just right"); + assert.ok(composer.get("titleLengthValid"), "in the range is okay"); }); QUnit.test("Title length for private messages", assert => { Discourse.SiteSettings.min_personal_message_title_length = 5; Discourse.SiteSettings.max_topic_title_length = 10; - const composer = createComposer({action: Composer.PRIVATE_MESSAGE}); + const composer = createComposer({ action: Composer.PRIVATE_MESSAGE }); - composer.set('title', 'asdf'); - assert.ok(!composer.get('titleLengthValid'), "short titles are not valid"); + composer.set("title", "asdf"); + assert.ok(!composer.get("titleLengthValid"), "short titles are not valid"); - composer.set('title', 'this is a long title'); - assert.ok(!composer.get('titleLengthValid'), "long titles are not valid"); + composer.set("title", "this is a long title"); + assert.ok(!composer.get("titleLengthValid"), "long titles are not valid"); - composer.set('title', 'just right'); - assert.ok(composer.get('titleLengthValid'), "in the range is okay"); + composer.set("title", "just right"); + assert.ok(composer.get("titleLengthValid"), "in the range is okay"); }); QUnit.test("Title length for private messages", assert => { Discourse.SiteSettings.min_personal_message_title_length = 5; Discourse.SiteSettings.max_topic_title_length = 10; - const composer = createComposer({action: Composer.PRIVATE_MESSAGE}); + const composer = createComposer({ action: Composer.PRIVATE_MESSAGE }); - composer.set('title', 'asdf'); - assert.ok(!composer.get('titleLengthValid'), "short titles are not valid"); + composer.set("title", "asdf"); + assert.ok(!composer.get("titleLengthValid"), "short titles are not valid"); - composer.set('title', 'this is a long title'); - assert.ok(!composer.get('titleLengthValid'), "long titles are not valid"); + composer.set("title", "this is a long title"); + assert.ok(!composer.get("titleLengthValid"), "long titles are not valid"); - composer.set('title', 'just right'); - assert.ok(composer.get('titleLengthValid'), "in the range is okay"); + composer.set("title", "just right"); + assert.ok(composer.get("titleLengthValid"), "in the range is okay"); }); QUnit.test("Post length for private messages with non human users", assert => { @@ -164,59 +237,88 @@ QUnit.test("Post length for private messages with non human users", assert => { topic: Ember.Object.create({ pm_with_non_human_user: true }) }); - assert.equal(composer.get('minimumPostLength'), 1); + assert.equal(composer.get("minimumPostLength"), 1); }); -QUnit.test('editingFirstPost', assert => { +QUnit.test("editingFirstPost", assert => { const composer = createComposer(); - assert.ok(!composer.get('editingFirstPost'), "it's false by default"); + assert.ok(!composer.get("editingFirstPost"), "it's false by default"); - const post = Discourse.Post.create({id: 123, post_number: 2}); - composer.setProperties({post: post, action: Composer.EDIT }); - assert.ok(!composer.get('editingFirstPost'), "it's false when not editing the first post"); - - post.set('post_number', 1); - assert.ok(composer.get('editingFirstPost'), "it's true when editing the first post"); + const post = Discourse.Post.create({ id: 123, post_number: 2 }); + composer.setProperties({ post: post, action: Composer.EDIT }); + assert.ok( + !composer.get("editingFirstPost"), + "it's false when not editing the first post" + ); + post.set("post_number", 1); + assert.ok( + composer.get("editingFirstPost"), + "it's true when editing the first post" + ); }); -QUnit.test('clearState', assert => { +QUnit.test("clearState", assert => { const composer = createComposer({ - originalText: 'asdf', - reply: 'asdf2', - post: Discourse.Post.create({id: 1}), - title: 'wat' + originalText: "asdf", + reply: "asdf2", + post: Discourse.Post.create({ id: 1 }), + title: "wat" }); composer.clearState(); - assert.blank(composer.get('originalText')); - assert.blank(composer.get('reply')); - assert.blank(composer.get('post')); - assert.blank(composer.get('title')); - + assert.blank(composer.get("originalText")); + assert.blank(composer.get("reply")); + assert.blank(composer.get("post")); + assert.blank(composer.get("title")); }); -QUnit.test('initial category when uncategorized is allowed', assert => { +QUnit.test("initial category when uncategorized is allowed", assert => { Discourse.SiteSettings.allow_uncategorized_topics = true; - const composer = openComposer({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); - assert.ok(!composer.get('categoryId'), "Uncategorized by default"); + const composer = openComposer({ + action: "createTopic", + draftKey: "asfd", + draftSequence: 1 + }); + assert.ok(!composer.get("categoryId"), "Uncategorized by default"); }); -QUnit.test('initial category when uncategorized is not allowed', assert => { +QUnit.test("initial category when uncategorized is not allowed", assert => { Discourse.SiteSettings.allow_uncategorized_topics = false; - const composer = openComposer({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); - assert.ok(!composer.get('categoryId'), "Uncategorized by default. Must choose a category."); + const composer = openComposer({ + action: "createTopic", + draftKey: "asfd", + draftSequence: 1 + }); + assert.ok( + !composer.get("categoryId"), + "Uncategorized by default. Must choose a category." + ); }); -QUnit.test('open with a quote', assert => { - const quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]'; +QUnit.test("open with a quote", assert => { + const quote = + '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]'; const newComposer = function() { - return openComposer({action: Composer.REPLY, draftKey: 'asfd', draftSequence: 1, quote: quote}); + return openComposer({ + action: Composer.REPLY, + draftKey: "asfd", + draftSequence: 1, + quote: quote + }); }; - assert.equal(newComposer().get('originalText'), quote, "originalText is the quote" ); - assert.equal(newComposer().get('replyDirty'), false, "replyDirty is initally false with a quote" ); + assert.equal( + newComposer().get("originalText"), + quote, + "originalText is the quote" + ); + assert.equal( + newComposer().get("replyDirty"), + false, + "replyDirty is initally false with a quote" + ); }); QUnit.test("Title length for static page topics as admin", assert => { @@ -224,42 +326,69 @@ QUnit.test("Title length for static page topics as admin", assert => { Discourse.SiteSettings.max_topic_title_length = 10; const composer = createComposer(); - const post = Discourse.Post.create({id: 123, post_number: 2, static_doc: true}); - composer.setProperties({post: post, action: Composer.EDIT }); + const post = Discourse.Post.create({ + id: 123, + post_number: 2, + static_doc: true + }); + composer.setProperties({ post: post, action: Composer.EDIT }); - composer.set('title', 'asdf'); - assert.ok(composer.get('titleLengthValid'), "admins can use short titles"); + composer.set("title", "asdf"); + assert.ok(composer.get("titleLengthValid"), "admins can use short titles"); - composer.set('title', 'this is a long title'); - assert.ok(composer.get('titleLengthValid'), "admins can use long titles"); + composer.set("title", "this is a long title"); + assert.ok(composer.get("titleLengthValid"), "admins can use long titles"); - composer.set('title', 'just right'); - assert.ok(composer.get('titleLengthValid'), "in the range is okay"); + composer.set("title", "just right"); + assert.ok(composer.get("titleLengthValid"), "in the range is okay"); - composer.set('title', ''); - assert.ok(!composer.get('titleLengthValid'), "admins must set title to at least 1 character"); + composer.set("title", ""); + assert.ok( + !composer.get("titleLengthValid"), + "admins must set title to at least 1 character" + ); }); QUnit.test("title placeholder depends on what you're doing", assert => { - let composer = createComposer({action: Composer.CREATE_TOPIC}); - assert.equal(composer.get('titlePlaceholder'), 'composer.title_placeholder', "placeholder for normal topic"); + let composer = createComposer({ action: Composer.CREATE_TOPIC }); + assert.equal( + composer.get("titlePlaceholder"), + "composer.title_placeholder", + "placeholder for normal topic" + ); - composer = createComposer({action: Composer.PRIVATE_MESSAGE}); - assert.equal(composer.get('titlePlaceholder'), 'composer.title_placeholder', "placeholder for private message"); + composer = createComposer({ action: Composer.PRIVATE_MESSAGE }); + assert.equal( + composer.get("titlePlaceholder"), + "composer.title_placeholder", + "placeholder for private message" + ); Discourse.SiteSettings.topic_featured_link_enabled = true; - composer = createComposer({action: Composer.CREATE_TOPIC}); - assert.equal(composer.get('titlePlaceholder'), 'composer.title_or_link_placeholder', "placeholder invites you to paste a link"); + composer = createComposer({ action: Composer.CREATE_TOPIC }); + assert.equal( + composer.get("titlePlaceholder"), + "composer.title_or_link_placeholder", + "placeholder invites you to paste a link" + ); - composer = createComposer({action: Composer.PRIVATE_MESSAGE}); - assert.equal(composer.get('titlePlaceholder'), 'composer.title_placeholder', "placeholder for private message with topic links enabled"); + composer = createComposer({ action: Composer.PRIVATE_MESSAGE }); + assert.equal( + composer.get("titlePlaceholder"), + "composer.title_placeholder", + "placeholder for private message with topic links enabled" + ); }); QUnit.test("allows featured link before choosing a category", assert => { Discourse.SiteSettings.topic_featured_link_enabled = true; Discourse.SiteSettings.allow_uncategorized_topics = false; - let composer = createComposer({action: Composer.CREATE_TOPIC}); - assert.equal(composer.get('titlePlaceholder'), 'composer.title_or_link_placeholder', "placeholder invites you to paste a link"); - assert.ok(composer.get('canEditTopicFeaturedLink'), "can paste link"); -}); \ No newline at end of file + let composer = createComposer({ action: Composer.CREATE_TOPIC }); + assert.equal( + composer.get("titlePlaceholder"), + "composer.title_or_link_placeholder", + "placeholder invites you to paste a link" + ); + assert.ok(composer.get("canEditTopicFeaturedLink"), "can paste link"); +}); diff --git a/test/javascripts/models/email-log-test.js.es6 b/test/javascripts/models/email-log-test.js.es6 index be18f9190f..e322da5a90 100644 --- a/test/javascripts/models/email-log-test.js.es6 +++ b/test/javascripts/models/email-log-test.js.es6 @@ -1,7 +1,7 @@ -import EmailLog from 'admin/models/email-log'; +import EmailLog from "admin/models/email-log"; QUnit.module("Discourse.EmailLog"); QUnit.test("create", assert => { assert.ok(EmailLog.create(), "it can be created without arguments"); -}); \ No newline at end of file +}); diff --git a/test/javascripts/models/group-test.js.es6 b/test/javascripts/models/group-test.js.es6 index 6b1b726dd5..33cb5a709c 100644 --- a/test/javascripts/models/group-test.js.es6 +++ b/test/javascripts/models/group-test.js.es6 @@ -1,13 +1,21 @@ -import Group from 'discourse/models/group'; +import Group from "discourse/models/group"; QUnit.module("model:group"); -QUnit.test('displayName', assert => { - const group = Group.create({ name: "test", display_name: 'donkey' }); +QUnit.test("displayName", assert => { + const group = Group.create({ name: "test", display_name: "donkey" }); - assert.equal(group.get('displayName'), "donkey", 'it should return the display name'); + assert.equal( + group.get("displayName"), + "donkey", + "it should return the display name" + ); - group.set('display_name', null); + group.set("display_name", null); - assert.equal(group.get('displayName'), "test", "it should return the group's name"); + assert.equal( + group.get("displayName"), + "test", + "it should return the group's name" + ); }); diff --git a/test/javascripts/models/invite-test.js.es6 b/test/javascripts/models/invite-test.js.es6 index 0cdc361fca..323375d42b 100644 --- a/test/javascripts/models/invite-test.js.es6 +++ b/test/javascripts/models/invite-test.js.es6 @@ -1,7 +1,7 @@ -import Invite from 'discourse/models/invite'; +import Invite from "discourse/models/invite"; QUnit.module("model:invite"); QUnit.test("create", assert => { assert.ok(Invite.create(), "it can be created without arguments"); -}); \ No newline at end of file +}); diff --git a/test/javascripts/models/model-test.js.es6 b/test/javascripts/models/model-test.js.es6 index dba22bd390..f5e81acae7 100644 --- a/test/javascripts/models/model-test.js.es6 +++ b/test/javascripts/models/model-test.js.es6 @@ -1,21 +1,35 @@ -import Model from 'discourse/models/model'; +import Model from "discourse/models/model"; QUnit.module("model:discourse"); -QUnit.test("extractByKey: converts a list of hashes into a hash of instances of specified class, indexed by their ids", assert => { - var firstObject = {id: "id_1", foo: "foo_1"}; - var secondObject = {id: "id_2", foo: "foo_2"}; +QUnit.test( + "extractByKey: converts a list of hashes into a hash of instances of specified class, indexed by their ids", + assert => { + var firstObject = { id: "id_1", foo: "foo_1" }; + var secondObject = { id: "id_2", foo: "foo_2" }; - var actual = Model.extractByKey([firstObject, secondObject], Ember.Object); - var expected = { - id_1: Ember.Object.create(firstObject), - id_2: Ember.Object.create(secondObject) - }; + var actual = Model.extractByKey([firstObject, secondObject], Ember.Object); + var expected = { + id_1: Ember.Object.create(firstObject), + id_2: Ember.Object.create(secondObject) + }; - assert.ok(_.isEqual(actual, expected)); -}); + assert.ok(_.isEqual(actual, expected)); + } +); -QUnit.test("extractByKey: returns an empty hash if there isn't anything to convert", assert => { - assert.deepEqual(Model.extractByKey(), {}, "when called without parameters"); - assert.deepEqual(Model.extractByKey([]), {}, "when called with an empty array"); -}); +QUnit.test( + "extractByKey: returns an empty hash if there isn't anything to convert", + assert => { + assert.deepEqual( + Model.extractByKey(), + {}, + "when called without parameters" + ); + assert.deepEqual( + Model.extractByKey([]), + {}, + "when called with an empty array" + ); + } +); diff --git a/test/javascripts/models/nav-item-test.js.es6 b/test/javascripts/models/nav-item-test.js.es6 index 38174eb639..115417e4d4 100644 --- a/test/javascripts/models/nav-item-test.js.es6 +++ b/test/javascripts/models/nav-item-test.js.es6 @@ -1,25 +1,32 @@ -import createStore from 'helpers/create-store'; +import createStore from "helpers/create-store"; QUnit.module("Discourse.NavItem", { beforeEach() { Ember.run(function() { - const asianCategory = Discourse.Category.create({name: '确实是这样', id: 343434}); - Discourse.Site.currentProp('categories').addObject(asianCategory); + const asianCategory = Discourse.Category.create({ + name: "确实是这样", + id: 343434 + }); + Discourse.Site.currentProp("categories").addObject(asianCategory); }); } }); -QUnit.test('href', assert =>{ +QUnit.test("href", assert => { assert.expect(4); function href(text, expected, label) { - assert.equal(Discourse.NavItem.fromText(text, {}).get('href'), expected, label); + assert.equal( + Discourse.NavItem.fromText(text, {}).get("href"), + expected, + label + ); } - href('latest', '/latest', 'latest'); - href('categories', '/categories', 'categories'); - href('category/bug', '/c/bug', 'English category name'); - href('category/确实是这样', '/c/343434-category', 'Chinese category name'); + href("latest", "/latest", "latest"); + href("categories", "/categories", "categories"); + href("category/bug", "/c/bug", "English category name"); + href("category/确实是这样", "/c/343434-category", "Chinese category name"); }); QUnit.test("count", assert => { @@ -31,5 +38,9 @@ QUnit.test("count", assert => { tracker.states["t1"] = { topic_id: 1, last_read_post_number: null }; tracker.incrementMessageCount(); - assert.equal(navItem.get("count"), 1, "it updates when a new message arrives"); + assert.equal( + navItem.get("count"), + 1, + "it updates when a new message arrives" + ); }); diff --git a/test/javascripts/models/post-stream-test.js.es6 b/test/javascripts/models/post-stream-test.js.es6 index 7418102a17..c33e9d6d51 100644 --- a/test/javascripts/models/post-stream-test.js.es6 +++ b/test/javascripts/models/post-stream-test.js.es6 @@ -1,85 +1,145 @@ QUnit.module("model:post-stream"); -import createStore from 'helpers/create-store'; +import createStore from "helpers/create-store"; const buildStream = function(id, stream) { const store = createStore(); - const topic = store.createRecord('topic', {id, chunk_size: 5}); - const ps = topic.get('postStream'); + const topic = store.createRecord("topic", { id, chunk_size: 5 }); + const ps = topic.get("postStream"); if (stream) { - ps.set('stream', stream); + ps.set("stream", stream); } return ps; }; -const participant = {username: 'eviltrout'}; +const participant = { username: "eviltrout" }; -QUnit.test('create', assert => { +QUnit.test("create", assert => { const store = createStore(); - assert.ok(store.createRecord('postStream'), 'it can be created with no parameters'); + assert.ok( + store.createRecord("postStream"), + "it can be created with no parameters" + ); }); -QUnit.test('defaults', assert => { +QUnit.test("defaults", assert => { const postStream = buildStream(1234); - assert.blank(postStream.get('posts'), "there are no posts in a stream by default"); - assert.ok(!postStream.get('loaded'), "it has never loaded"); - assert.present(postStream.get('topic')); + assert.blank( + postStream.get("posts"), + "there are no posts in a stream by default" + ); + assert.ok(!postStream.get("loaded"), "it has never loaded"); + assert.present(postStream.get("topic")); }); -QUnit.test('appending posts', assert => { +QUnit.test("appending posts", assert => { const postStream = buildStream(4567, [1, 3, 4]); const store = postStream.store; - assert.equal(postStream.get('firstPostId'), 1); - assert.equal(postStream.get('lastPostId'), 4, "the last post id is 4"); + assert.equal(postStream.get("firstPostId"), 1); + assert.equal(postStream.get("lastPostId"), 4, "the last post id is 4"); - assert.ok(!postStream.get('hasPosts'), "there are no posts by default"); - assert.ok(!postStream.get('firstPostPresent'), "the first post is not loaded"); - assert.ok(!postStream.get('loadedAllPosts'), "the last post is not loaded"); - assert.equal(postStream.get('posts.length'), 0, "it has no posts initially"); + assert.ok(!postStream.get("hasPosts"), "there are no posts by default"); + assert.ok( + !postStream.get("firstPostPresent"), + "the first post is not loaded" + ); + assert.ok(!postStream.get("loadedAllPosts"), "the last post is not loaded"); + assert.equal(postStream.get("posts.length"), 0, "it has no posts initially"); - postStream.appendPost(store.createRecord('post', {id: 2, post_number: 2})); - assert.ok(!postStream.get('firstPostPresent'), "the first post is still not loaded"); - assert.equal(postStream.get('posts.length'), 1, "it has one post in the stream"); + postStream.appendPost(store.createRecord("post", { id: 2, post_number: 2 })); + assert.ok( + !postStream.get("firstPostPresent"), + "the first post is still not loaded" + ); + assert.equal( + postStream.get("posts.length"), + 1, + "it has one post in the stream" + ); - postStream.appendPost(store.createRecord('post', {id: 4, post_number: 4})); - assert.ok(!postStream.get('firstPostPresent'), "the first post is still loaded"); - assert.ok(postStream.get('loadedAllPosts'), "the last post is now loaded"); - assert.equal(postStream.get('posts.length'), 2, "it has two posts in the stream"); + postStream.appendPost(store.createRecord("post", { id: 4, post_number: 4 })); + assert.ok( + !postStream.get("firstPostPresent"), + "the first post is still loaded" + ); + assert.ok(postStream.get("loadedAllPosts"), "the last post is now loaded"); + assert.equal( + postStream.get("posts.length"), + 2, + "it has two posts in the stream" + ); - postStream.appendPost(store.createRecord('post', {id: 4, post_number: 4})); - assert.equal(postStream.get('posts.length'), 2, "it will not add the same post with id twice"); + postStream.appendPost(store.createRecord("post", { id: 4, post_number: 4 })); + assert.equal( + postStream.get("posts.length"), + 2, + "it will not add the same post with id twice" + ); - const stagedPost = store.createRecord('post', {raw: 'incomplete post'}); + const stagedPost = store.createRecord("post", { raw: "incomplete post" }); postStream.appendPost(stagedPost); - assert.equal(postStream.get('posts.length'), 3, "it can handle posts without ids"); + assert.equal( + postStream.get("posts.length"), + 3, + "it can handle posts without ids" + ); postStream.appendPost(stagedPost); - assert.equal(postStream.get('posts.length'), 3, "it won't add the same post without an id twice"); + assert.equal( + postStream.get("posts.length"), + 3, + "it won't add the same post without an id twice" + ); // change the stream - postStream.set('stream', [1, 2, 4]); - assert.ok(!postStream.get('firstPostPresent'), "the first post no longer loaded since the stream changed."); - assert.ok(postStream.get('loadedAllPosts'), "the last post is still the last post in the new stream"); + postStream.set("stream", [1, 2, 4]); + assert.ok( + !postStream.get("firstPostPresent"), + "the first post no longer loaded since the stream changed." + ); + assert.ok( + postStream.get("loadedAllPosts"), + "the last post is still the last post in the new stream" + ); }); -QUnit.test('closestPostNumberFor', assert => { +QUnit.test("closestPostNumberFor", assert => { const postStream = buildStream(1231); const store = postStream.store; - assert.blank(postStream.closestPostNumberFor(1), "there is no closest post when nothing is loaded"); + assert.blank( + postStream.closestPostNumberFor(1), + "there is no closest post when nothing is loaded" + ); - postStream.appendPost(store.createRecord('post', {id: 1, post_number: 2})); - postStream.appendPost(store.createRecord('post', {id: 2, post_number: 3})); + postStream.appendPost(store.createRecord("post", { id: 1, post_number: 2 })); + postStream.appendPost(store.createRecord("post", { id: 2, post_number: 3 })); - assert.equal(postStream.closestPostNumberFor(2), 2, "If a post is in the stream it returns its post number"); - assert.equal(postStream.closestPostNumberFor(3), 3, "If a post is in the stream it returns its post number"); - assert.equal(postStream.closestPostNumberFor(10), 3, "it clips to the upper bound of the stream"); - assert.equal(postStream.closestPostNumberFor(0), 2, "it clips to the lower bound of the stream"); + assert.equal( + postStream.closestPostNumberFor(2), + 2, + "If a post is in the stream it returns its post number" + ); + assert.equal( + postStream.closestPostNumberFor(3), + 3, + "If a post is in the stream it returns its post number" + ); + assert.equal( + postStream.closestPostNumberFor(10), + 3, + "it clips to the upper bound of the stream" + ); + assert.equal( + postStream.closestPostNumberFor(0), + 2, + "it clips to the lower bound of the stream" + ); }); -QUnit.test('closestDaysAgoFor', assert => { +QUnit.test("closestDaysAgoFor", assert => { const postStream = buildStream(1231); - postStream.set('timelineLookup', [[1, 10], [3, 8], [5, 1]]); + postStream.set("timelineLookup", [[1, 10], [3, 8], [5, 1]]); assert.equal(postStream.closestDaysAgoFor(1), 10); assert.equal(postStream.closestDaysAgoFor(2), 10); @@ -93,35 +153,35 @@ QUnit.test('closestDaysAgoFor', assert => { assert.equal(postStream.closestDaysAgoFor(10), 1); }); -QUnit.test('closestDaysAgoFor - empty', assert => { +QUnit.test("closestDaysAgoFor - empty", assert => { const postStream = buildStream(1231); - postStream.set('timelineLookup', []); + postStream.set("timelineLookup", []); assert.equal(postStream.closestDaysAgoFor(1), null); }); -QUnit.test('updateFromJson', assert => { +QUnit.test("updateFromJson", assert => { const postStream = buildStream(1231); postStream.updateFromJson({ - posts: [{id: 1}], + posts: [{ id: 1 }], stream: [1], extra_property: 12 }); - assert.equal(postStream.get('posts.length'), 1, 'it loaded the posts'); - assert.containsInstance(postStream.get('posts'), Discourse.Post); + assert.equal(postStream.get("posts.length"), 1, "it loaded the posts"); + assert.containsInstance(postStream.get("posts"), Discourse.Post); - assert.equal(postStream.get('extra_property'), 12); + assert.equal(postStream.get("extra_property"), 12); }); QUnit.test("removePosts", assert => { - const postStream = buildStream(10000001, [1,2,3]); + const postStream = buildStream(10000001, [1, 2, 3]); const store = postStream.store; - const p1 = store.createRecord('post', {id: 1, post_number: 2}), - p2 = store.createRecord('post', {id: 2, post_number: 3}), - p3 = store.createRecord('post', {id: 3, post_number: 4}); + const p1 = store.createRecord("post", { id: 1, post_number: 2 }), + p2 = store.createRecord("post", { id: 2, post_number: 3 }), + p3 = store.createRecord("post", { id: 3, post_number: 4 }); postStream.appendPost(p1); postStream.appendPost(p2); @@ -129,12 +189,11 @@ QUnit.test("removePosts", assert => { // Removing nothing does nothing postStream.removePosts(); - assert.equal(postStream.get('posts.length'), 3); + assert.equal(postStream.get("posts.length"), 3); postStream.removePosts([p1, p3]); - assert.equal(postStream.get('posts.length'), 1); - assert.deepEqual(postStream.get('stream'), [2]); - + assert.equal(postStream.get("posts.length"), 1); + assert.deepEqual(postStream.get("stream"), [2]); }); QUnit.test("cancelFilter", assert => { @@ -142,156 +201,283 @@ QUnit.test("cancelFilter", assert => { sandbox.stub(postStream, "refresh").returns(new Ember.RSVP.resolve()); - postStream.set('summary', true); + postStream.set("summary", true); postStream.cancelFilter(); - assert.ok(!postStream.get('summary'), "summary is cancelled"); + assert.ok(!postStream.get("summary"), "summary is cancelled"); postStream.toggleParticipant(participant); postStream.cancelFilter(); - assert.blank(postStream.get('userFilters'), "cancelling the filters clears the userFilters"); + assert.blank( + postStream.get("userFilters"), + "cancelling the filters clears the userFilters" + ); }); QUnit.test("findPostIdForPostNumber", assert => { const postStream = buildStream(1234, [10, 20, 30, 40, 50, 60, 70]); - postStream.set('gaps', { before: { 60: [55, 58] } }); - - assert.equal(postStream.findPostIdForPostNumber(500), null, 'it returns null when the post cannot be found'); - assert.equal(postStream.findPostIdForPostNumber(1), 10, 'it finds the postId at the beginning'); - assert.equal(postStream.findPostIdForPostNumber(5), 50, 'it finds the postId in the middle'); - assert.equal(postStream.findPostIdForPostNumber(8), 60, 'it respects gaps'); + postStream.set("gaps", { before: { 60: [55, 58] } }); + assert.equal( + postStream.findPostIdForPostNumber(500), + null, + "it returns null when the post cannot be found" + ); + assert.equal( + postStream.findPostIdForPostNumber(1), + 10, + "it finds the postId at the beginning" + ); + assert.equal( + postStream.findPostIdForPostNumber(5), + 50, + "it finds the postId in the middle" + ); + assert.equal(postStream.findPostIdForPostNumber(8), 60, "it respects gaps"); }); QUnit.test("toggleParticipant", assert => { const postStream = buildStream(1236); sandbox.stub(postStream, "refresh").returns(new Ember.RSVP.resolve()); - assert.equal(postStream.get('userFilters.length'), 0, "by default no participants are toggled"); + assert.equal( + postStream.get("userFilters.length"), + 0, + "by default no participants are toggled" + ); postStream.toggleParticipant(participant.username); - assert.ok(postStream.get('userFilters').includes('eviltrout'), 'eviltrout is in the filters'); + assert.ok( + postStream.get("userFilters").includes("eviltrout"), + "eviltrout is in the filters" + ); postStream.toggleParticipant(participant.username); - assert.blank(postStream.get('userFilters'), "toggling the participant again removes them"); + assert.blank( + postStream.get("userFilters"), + "toggling the participant again removes them" + ); }); QUnit.test("streamFilters", assert => { const postStream = buildStream(1237); sandbox.stub(postStream, "refresh").returns(new Ember.RSVP.resolve()); - assert.deepEqual(postStream.get('streamFilters'), {}, "there are no postFilters by default"); - assert.ok(postStream.get('hasNoFilters'), "there are no filters by default"); + assert.deepEqual( + postStream.get("streamFilters"), + {}, + "there are no postFilters by default" + ); + assert.ok(postStream.get("hasNoFilters"), "there are no filters by default"); - postStream.set('summary', true); - assert.deepEqual(postStream.get('streamFilters'), {filter: "summary"}, "postFilters contains the summary flag"); - assert.ok(!postStream.get('hasNoFilters'), "now there are filters present"); + postStream.set("summary", true); + assert.deepEqual( + postStream.get("streamFilters"), + { filter: "summary" }, + "postFilters contains the summary flag" + ); + assert.ok(!postStream.get("hasNoFilters"), "now there are filters present"); postStream.toggleParticipant(participant.username); - assert.deepEqual(postStream.get('streamFilters'), { - username_filters: 'eviltrout', - }, "streamFilters contains the username we filtered"); + assert.deepEqual( + postStream.get("streamFilters"), + { + username_filters: "eviltrout" + }, + "streamFilters contains the username we filtered" + ); }); QUnit.test("loading", assert => { let postStream = buildStream(1234); - assert.ok(!postStream.get('loading'), "we're not loading by default"); + assert.ok(!postStream.get("loading"), "we're not loading by default"); - postStream.set('loadingAbove', true); - assert.ok(postStream.get('loading'), "we're loading if loading above"); + postStream.set("loadingAbove", true); + assert.ok(postStream.get("loading"), "we're loading if loading above"); postStream = buildStream(1234); - postStream.set('loadingBelow', true); - assert.ok(postStream.get('loading'), "we're loading if loading below"); + postStream.set("loadingBelow", true); + assert.ok(postStream.get("loading"), "we're loading if loading below"); postStream = buildStream(1234); - postStream.set('loadingFilter', true); - assert.ok(postStream.get('loading'), "we're loading if loading a filter"); + postStream.set("loadingFilter", true); + assert.ok(postStream.get("loading"), "we're loading if loading a filter"); }); QUnit.test("nextWindow", assert => { - const postStream = buildStream(1234, [1,2,3,5,8,9,10,11,13,14,15,16]); + const postStream = buildStream(1234, [ + 1, + 2, + 3, + 5, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16 + ]); - assert.blank(postStream.get('nextWindow'), 'With no posts loaded, the window is blank'); + assert.blank( + postStream.get("nextWindow"), + "With no posts loaded, the window is blank" + ); - postStream.updateFromJson({ posts: [{id: 1}, {id: 2}] }); - assert.deepEqual(postStream.get('nextWindow'), [3,5,8,9,10], - "If we've loaded the first 2 posts, the window should be the 5 after that"); + postStream.updateFromJson({ posts: [{ id: 1 }, { id: 2 }] }); + assert.deepEqual( + postStream.get("nextWindow"), + [3, 5, 8, 9, 10], + "If we've loaded the first 2 posts, the window should be the 5 after that" + ); - postStream.updateFromJson({ posts: [{id: 13}] }); - assert.deepEqual(postStream.get('nextWindow'), [14, 15, 16], "Boundary check: stop at the end."); + postStream.updateFromJson({ posts: [{ id: 13 }] }); + assert.deepEqual( + postStream.get("nextWindow"), + [14, 15, 16], + "Boundary check: stop at the end." + ); - postStream.updateFromJson({ posts: [{id: 16}] }); - assert.blank(postStream.get('nextWindow'), "Once we've seen everything there's nothing to load."); + postStream.updateFromJson({ posts: [{ id: 16 }] }); + assert.blank( + postStream.get("nextWindow"), + "Once we've seen everything there's nothing to load." + ); }); QUnit.test("previousWindow", assert => { - const postStream = buildStream(1234, [1,2,3,5,8,9,10,11,13,14,15,16]); + const postStream = buildStream(1234, [ + 1, + 2, + 3, + 5, + 8, + 9, + 10, + 11, + 13, + 14, + 15, + 16 + ]); - assert.blank(postStream.get('previousWindow'), 'With no posts loaded, the window is blank'); + assert.blank( + postStream.get("previousWindow"), + "With no posts loaded, the window is blank" + ); - postStream.updateFromJson({ posts: [{id: 11}, {id: 13}] }); - assert.deepEqual(postStream.get('previousWindow'), [3, 5, 8, 9, 10], - "If we've loaded in the middle, it's the previous 5 posts"); + postStream.updateFromJson({ posts: [{ id: 11 }, { id: 13 }] }); + assert.deepEqual( + postStream.get("previousWindow"), + [3, 5, 8, 9, 10], + "If we've loaded in the middle, it's the previous 5 posts" + ); - postStream.updateFromJson({ posts: [{id: 3}] }); - assert.deepEqual(postStream.get('previousWindow'), [1, 2], "Boundary check: stop at the beginning."); + postStream.updateFromJson({ posts: [{ id: 3 }] }); + assert.deepEqual( + postStream.get("previousWindow"), + [1, 2], + "Boundary check: stop at the beginning." + ); - postStream.updateFromJson({ posts: [{id: 1}] }); - assert.blank(postStream.get('previousWindow'), "Once we've seen everything there's nothing to load."); + postStream.updateFromJson({ posts: [{ id: 1 }] }); + assert.blank( + postStream.get("previousWindow"), + "Once we've seen everything there's nothing to load." + ); }); QUnit.test("storePost", assert => { const postStream = buildStream(1234), - store = postStream.store, - post = store.createRecord('post', {id: 1, post_number: 100, raw: 'initial value'}); + store = postStream.store, + post = store.createRecord("post", { + id: 1, + post_number: 100, + raw: "initial value" + }); - assert.blank(postStream.get('topic.highest_post_number'), "it has no highest post number yet"); + assert.blank( + postStream.get("topic.highest_post_number"), + "it has no highest post number yet" + ); let stored = postStream.storePost(post); assert.equal(post, stored, "it returns the post it stored"); - assert.equal(post.get('topic'), postStream.get('topic'), "it creates the topic reference properly"); - assert.equal(postStream.get('topic.highest_post_number'), 100, "it set the highest post number"); + assert.equal( + post.get("topic"), + postStream.get("topic"), + "it creates the topic reference properly" + ); + assert.equal( + postStream.get("topic.highest_post_number"), + 100, + "it set the highest post number" + ); - const dupePost = store.createRecord('post', {id: 1, post_number: 100, raw: 'updated value'}); + const dupePost = store.createRecord("post", { + id: 1, + post_number: 100, + raw: "updated value" + }); const storedDupe = postStream.storePost(dupePost); - assert.equal(storedDupe, post, "it returns the previously stored post instead to avoid dupes"); - assert.equal(storedDupe.get('raw'), 'updated value', 'it updates the previously stored post'); + assert.equal( + storedDupe, + post, + "it returns the previously stored post instead to avoid dupes" + ); + assert.equal( + storedDupe.get("raw"), + "updated value", + "it updates the previously stored post" + ); - const postWithoutId = store.createRecord('post', {raw: 'hello world'}); + const postWithoutId = store.createRecord("post", { raw: "hello world" }); stored = postStream.storePost(postWithoutId); assert.equal(stored, postWithoutId, "it returns the same post back"); - }); QUnit.test("identity map", assert => { const postStream = buildStream(1234); const store = postStream.store; - const p1 = postStream.appendPost(store.createRecord('post', {id: 1, post_number: 1})); - const p3 = postStream.appendPost(store.createRecord('post', {id: 3, post_number: 4})); + const p1 = postStream.appendPost( + store.createRecord("post", { id: 1, post_number: 1 }) + ); + const p3 = postStream.appendPost( + store.createRecord("post", { id: 3, post_number: 4 }) + ); - assert.equal(postStream.findLoadedPost(1), p1, "it can return cached posts by id"); + assert.equal( + postStream.findLoadedPost(1), + p1, + "it can return cached posts by id" + ); assert.blank(postStream.findLoadedPost(4), "it can't find uncached posts"); // Find posts by ids uses the identity map return postStream.findPostsByIds([1, 2, 3]).then(result => { assert.equal(result.length, 3); assert.equal(result.objectAt(0), p1); - assert.equal(result.objectAt(1).get('post_number'), 2); + assert.equal(result.objectAt(1).get("post_number"), 2); assert.equal(result.objectAt(2), p3); }); }); QUnit.test("loadIntoIdentityMap with no data", assert => { - return buildStream(1234).loadIntoIdentityMap([]).then(result => { - assert.equal(result.length, 0, 'requesting no posts produces no posts'); - }); + return buildStream(1234) + .loadIntoIdentityMap([]) + .then(result => { + assert.equal(result.length, 0, "requesting no posts produces no posts"); + }); }); QUnit.test("loadIntoIdentityMap with post ids", assert => { const postStream = buildStream(1234); return postStream.loadIntoIdentityMap([10]).then(function() { - assert.present(postStream.findLoadedPost(10), "it adds the returned post to the store"); + assert.present( + postStream.findLoadedPost(10), + "it adds the returned post to the store" + ); }); }); @@ -299,14 +485,29 @@ QUnit.test("staging and undoing a new post", assert => { const postStream = buildStream(10101, [1]); const store = postStream.store; - const original = store.createRecord('post', {id: 1, post_number: 1, topic_id: 10101}); + const original = store.createRecord("post", { + id: 1, + post_number: 1, + topic_id: 10101 + }); postStream.appendPost(original); - assert.ok(postStream.get('lastAppended'), original, "the original post is lastAppended"); + assert.ok( + postStream.get("lastAppended"), + original, + "the original post is lastAppended" + ); - const user = Discourse.User.create({username: 'eviltrout', name: 'eviltrout', id: 321}); - const stagedPost = store.createRecord('post', { raw: 'hello world this is my new post', topic_id: 10101 }); + const user = Discourse.User.create({ + username: "eviltrout", + name: "eviltrout", + id: 321 + }); + const stagedPost = store.createRecord("post", { + raw: "hello world this is my new post", + topic_id: 10101 + }); - const topic = postStream.get('topic'); + const topic = postStream.get("topic"); topic.setProperties({ posts_count: 1, highest_post_number: 1 @@ -315,67 +516,149 @@ QUnit.test("staging and undoing a new post", assert => { // Stage the new post in the stream const result = postStream.stagePost(stagedPost, user); assert.equal(result, "staged", "it returns staged"); - assert.equal(topic.get('highest_post_number'), 2, "it updates the highest_post_number"); - assert.ok(postStream.get('loading'), "it is loading while the post is being staged"); - assert.ok(postStream.get('lastAppended'), original, "it doesn't consider staged posts as the lastAppended"); + assert.equal( + topic.get("highest_post_number"), + 2, + "it updates the highest_post_number" + ); + assert.ok( + postStream.get("loading"), + "it is loading while the post is being staged" + ); + assert.ok( + postStream.get("lastAppended"), + original, + "it doesn't consider staged posts as the lastAppended" + ); - assert.equal(topic.get('posts_count'), 2, "it increases the post count"); - assert.present(topic.get('last_posted_at'), "it updates last_posted_at"); - assert.equal(topic.get('details.last_poster'), user, "it changes the last poster"); + assert.equal(topic.get("posts_count"), 2, "it increases the post count"); + assert.present(topic.get("last_posted_at"), "it updates last_posted_at"); + assert.equal( + topic.get("details.last_poster"), + user, + "it changes the last poster" + ); - assert.equal(stagedPost.get('topic'), topic, "it assigns the topic reference"); - assert.equal(stagedPost.get('post_number'), 2, "it is assigned the probable post_number"); - assert.present(stagedPost.get('created_at'), "it is assigned a created date"); - assert.ok(postStream.get('posts').includes(stagedPost), "the post is added to the stream"); - assert.equal(stagedPost.get('id'), -1, "the post has a magical -1 id"); + assert.equal( + stagedPost.get("topic"), + topic, + "it assigns the topic reference" + ); + assert.equal( + stagedPost.get("post_number"), + 2, + "it is assigned the probable post_number" + ); + assert.present(stagedPost.get("created_at"), "it is assigned a created date"); + assert.ok( + postStream.get("posts").includes(stagedPost), + "the post is added to the stream" + ); + assert.equal(stagedPost.get("id"), -1, "the post has a magical -1 id"); // Undoing a created post (there was an error) postStream.undoPost(stagedPost); - assert.ok(!postStream.get('loading'), "it is no longer loading"); - assert.equal(topic.get('highest_post_number'), 1, "it reverts the highest_post_number"); - assert.equal(topic.get('posts_count'), 1, "it reverts the post count"); - assert.equal(postStream.get('filteredPostsCount'), 1, "it retains the filteredPostsCount"); - assert.ok(!postStream.get('posts').includes(stagedPost), "the post is removed from the stream"); - assert.ok(postStream.get('lastAppended'), original, "it doesn't consider undid post lastAppended"); + assert.ok(!postStream.get("loading"), "it is no longer loading"); + assert.equal( + topic.get("highest_post_number"), + 1, + "it reverts the highest_post_number" + ); + assert.equal(topic.get("posts_count"), 1, "it reverts the post count"); + assert.equal( + postStream.get("filteredPostsCount"), + 1, + "it retains the filteredPostsCount" + ); + assert.ok( + !postStream.get("posts").includes(stagedPost), + "the post is removed from the stream" + ); + assert.ok( + postStream.get("lastAppended"), + original, + "it doesn't consider undid post lastAppended" + ); }); QUnit.test("staging and committing a post", assert => { const postStream = buildStream(10101, [1]); const store = postStream.store; - const original = store.createRecord('post', {id: 1, post_number: 1, topic_id: 10101}); + const original = store.createRecord("post", { + id: 1, + post_number: 1, + topic_id: 10101 + }); postStream.appendPost(original); - assert.ok(postStream.get('lastAppended'), original, "the original post is lastAppended"); + assert.ok( + postStream.get("lastAppended"), + original, + "the original post is lastAppended" + ); - const user = Discourse.User.create({username: 'eviltrout', name: 'eviltrout', id: 321}); - const stagedPost = store.createRecord('post', { raw: 'hello world this is my new post', topic_id: 10101 }); + const user = Discourse.User.create({ + username: "eviltrout", + name: "eviltrout", + id: 321 + }); + const stagedPost = store.createRecord("post", { + raw: "hello world this is my new post", + topic_id: 10101 + }); - const topic = postStream.get('topic'); - topic.set('posts_count', 1); + const topic = postStream.get("topic"); + topic.set("posts_count", 1); // Stage the new post in the stream let result = postStream.stagePost(stagedPost, user); assert.equal(result, "staged", "it returns staged"); - assert.ok(postStream.get('loading'), "it is loading while the post is being staged"); + assert.ok( + postStream.get("loading"), + "it is loading while the post is being staged" + ); stagedPost.setProperties({ id: 1234, raw: "different raw value" }); result = postStream.stagePost(stagedPost, user); - assert.equal(result, "alreadyStaging", "you can't stage a post while it is currently staging"); - assert.ok(postStream.get('lastAppended'), original, "staging a post doesn't change the lastAppended"); + assert.equal( + result, + "alreadyStaging", + "you can't stage a post while it is currently staging" + ); + assert.ok( + postStream.get("lastAppended"), + original, + "staging a post doesn't change the lastAppended" + ); postStream.commitPost(stagedPost); - assert.ok(postStream.get('posts').includes(stagedPost), "the post is still in the stream"); - assert.ok(!postStream.get('loading'), "it is no longer loading"); + assert.ok( + postStream.get("posts").includes(stagedPost), + "the post is still in the stream" + ); + assert.ok(!postStream.get("loading"), "it is no longer loading"); - assert.equal(postStream.get('filteredPostsCount'), 2, "it increases the filteredPostsCount"); + assert.equal( + postStream.get("filteredPostsCount"), + 2, + "it increases the filteredPostsCount" + ); - const found = postStream.findLoadedPost(stagedPost.get('id')); + const found = postStream.findLoadedPost(stagedPost.get("id")); assert.present(found, "the post is in the identity map"); assert.ok(postStream.indexOf(stagedPost) > -1, "the post is in the stream"); - assert.equal(found.get('raw'), 'different raw value', 'it also updated the value in the stream'); - assert.ok(postStream.get('lastAppended'), found, "comitting a post changes lastAppended"); + assert.equal( + found.get("raw"), + "different raw value", + "it also updated the value in the stream" + ); + assert.ok( + postStream.get("lastAppended"), + found, + "comitting a post changes lastAppended" + ); }); QUnit.test("loadedAllPosts when the id changes", assert => { @@ -383,55 +666,74 @@ QUnit.test("loadedAllPosts when the id changes", assert => { // message bus. If the id of a post changes we should reconsider the loadedAllPosts property. const postStream = buildStream(10101, [1, 2]); const store = postStream.store; - const postWithoutId = store.createRecord('post', { raw: 'hello world this is my new post' }); + const postWithoutId = store.createRecord("post", { + raw: "hello world this is my new post" + }); - postStream.appendPost(store.createRecord('post', {id: 1, post_number: 1})); + postStream.appendPost(store.createRecord("post", { id: 1, post_number: 1 })); postStream.appendPost(postWithoutId); - assert.ok(!postStream.get('loadedAllPosts'), 'the last post is not loaded'); + assert.ok(!postStream.get("loadedAllPosts"), "the last post is not loaded"); - postWithoutId.set('id', 2); - assert.ok(postStream.get('loadedAllPosts'), 'the last post is loaded now that the post has an id'); + postWithoutId.set("id", 2); + assert.ok( + postStream.get("loadedAllPosts"), + "the last post is loaded now that the post has an id" + ); }); QUnit.test("comitting and triggerNewPostInStream race condition", assert => { const postStream = buildStream(4964); const store = postStream.store; - postStream.appendPost(store.createRecord('post', {id: 1, post_number: 1})); - const user = Discourse.User.create({username: 'eviltrout', name: 'eviltrout', id: 321}); - const stagedPost = store.createRecord('post', { raw: 'hello world this is my new post' }); + postStream.appendPost(store.createRecord("post", { id: 1, post_number: 1 })); + const user = Discourse.User.create({ + username: "eviltrout", + name: "eviltrout", + id: 321 + }); + const stagedPost = store.createRecord("post", { + raw: "hello world this is my new post" + }); postStream.stagePost(stagedPost, user); - assert.equal(postStream.get('filteredPostsCount'), 0, "it has no filteredPostsCount yet"); - stagedPost.set('id', 123); + assert.equal( + postStream.get("filteredPostsCount"), + 0, + "it has no filteredPostsCount yet" + ); + stagedPost.set("id", 123); - sandbox.stub(postStream, 'appendMore'); + sandbox.stub(postStream, "appendMore"); postStream.triggerNewPostInStream(123); - assert.equal(postStream.get('filteredPostsCount'), 1, "it added the post"); + assert.equal(postStream.get("filteredPostsCount"), 1, "it added the post"); postStream.commitPost(stagedPost); - assert.equal(postStream.get('filteredPostsCount'), 1, "it does not add the same post twice"); + assert.equal( + postStream.get("filteredPostsCount"), + 1, + "it does not add the same post twice" + ); }); QUnit.test("postsWithPlaceholders", assert => { const postStream = buildStream(4964, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); - const postsWithPlaceholders = postStream.get('postsWithPlaceholders'); + const postsWithPlaceholders = postStream.get("postsWithPlaceholders"); const store = postStream.store; const testProxy = Ember.ArrayProxy.create({ content: postsWithPlaceholders }); - const p1 = store.createRecord('post', {id: 1, post_number: 1}); - const p2 = store.createRecord('post', {id: 2, post_number: 2}); - const p3 = store.createRecord('post', {id: 3, post_number: 3}); - const p4 = store.createRecord('post', {id: 4, post_number: 4}); + const p1 = store.createRecord("post", { id: 1, post_number: 1 }); + const p2 = store.createRecord("post", { id: 2, post_number: 2 }); + const p3 = store.createRecord("post", { id: 3, post_number: 3 }); + const p4 = store.createRecord("post", { id: 4, post_number: 4 }); postStream.appendPost(p1); postStream.appendPost(p2); postStream.appendPost(p3); // Test enumerable and array access - assert.equal(postsWithPlaceholders.get('length'), 3); - assert.equal(testProxy.get('length'), 3); + assert.equal(postsWithPlaceholders.get("length"), 3); + assert.equal(testProxy.get("length"), 3); assert.equal(postsWithPlaceholders.nextObject(0), p1); assert.equal(postsWithPlaceholders.objectAt(0), p1); assert.equal(postsWithPlaceholders.nextObject(1, p1), p2); @@ -440,8 +742,12 @@ QUnit.test("postsWithPlaceholders", assert => { assert.equal(postsWithPlaceholders.objectAt(2), p3); const promise = postStream.appendMore(); - assert.equal(postsWithPlaceholders.get('length'), 8, 'we immediately have a larger placeholder window'); - assert.equal(testProxy.get('length'), 8); + assert.equal( + postsWithPlaceholders.get("length"), + 8, + "we immediately have a larger placeholder window" + ); + assert.equal(testProxy.get("length"), 8); assert.ok(!!postsWithPlaceholders.nextObject(3, p3)); assert.ok(!!postsWithPlaceholders.objectAt(4)); assert.ok(postsWithPlaceholders.objectAt(3) !== p4); @@ -449,9 +755,12 @@ QUnit.test("postsWithPlaceholders", assert => { return promise.then(() => { assert.equal(postsWithPlaceholders.objectAt(3), p4); - assert.equal(postsWithPlaceholders.get('length'), 8, 'have a larger placeholder window when loaded'); - assert.equal(testProxy.get('length'), 8); + assert.equal( + postsWithPlaceholders.get("length"), + 8, + "have a larger placeholder window when loaded" + ); + assert.equal(testProxy.get("length"), 8); assert.equal(testProxy.objectAt(3), p4); }); - }); diff --git a/test/javascripts/models/post-test.js.es6 b/test/javascripts/models/post-test.js.es6 index 22cb0bde24..2ded01501f 100644 --- a/test/javascripts/models/post-test.js.es6 +++ b/test/javascripts/models/post-test.js.es6 @@ -1,72 +1,96 @@ QUnit.module("Discourse.Post"); var buildPost = function(args) { - return Discourse.Post.create(_.merge({ - id: 1, - can_delete: true, - version: 1 - }, args || {})); + return Discourse.Post.create( + _.merge( + { + id: 1, + can_delete: true, + version: 1 + }, + args || {} + ) + ); }; -QUnit.test('defaults', assert => { - var post = Discourse.Post.create({id: 1}); - assert.blank(post.get('deleted_at'), "it has no deleted_at by default"); - assert.blank(post.get('deleted_by'), "there is no deleted_by by default"); +QUnit.test("defaults", assert => { + var post = Discourse.Post.create({ id: 1 }); + assert.blank(post.get("deleted_at"), "it has no deleted_at by default"); + assert.blank(post.get("deleted_by"), "there is no deleted_by by default"); }); -QUnit.test('new_user', assert => { - var post = Discourse.Post.create({trust_level: 0}); - assert.ok(post.get('new_user'), "post is from a new user"); +QUnit.test("new_user", assert => { + var post = Discourse.Post.create({ trust_level: 0 }); + assert.ok(post.get("new_user"), "post is from a new user"); - post.set('trust_level', 1); - assert.ok(!post.get('new_user'), "post is no longer from a new user"); + post.set("trust_level", 1); + assert.ok(!post.get("new_user"), "post is no longer from a new user"); }); -QUnit.test('firstPost', assert => { - var post = Discourse.Post.create({post_number: 1}); - assert.ok(post.get('firstPost'), "it's the first post"); +QUnit.test("firstPost", assert => { + var post = Discourse.Post.create({ post_number: 1 }); + assert.ok(post.get("firstPost"), "it's the first post"); - post.set('post_number', 10); - assert.ok(!post.get('firstPost'), "post is no longer the first post"); + post.set("post_number", 10); + assert.ok(!post.get("firstPost"), "post is no longer the first post"); }); -QUnit.test('updateFromPost', assert => { +QUnit.test("updateFromPost", assert => { var post = Discourse.Post.create({ post_number: 1, - raw: 'hello world' + raw: "hello world" }); - post.updateFromPost(Discourse.Post.create({ - raw: 'different raw', - wat: function() { return 123; } - })); + post.updateFromPost( + Discourse.Post.create({ + raw: "different raw", + wat: function() { + return 123; + } + }) + ); - assert.equal(post.get('raw'), "different raw", "raw field updated"); + assert.equal(post.get("raw"), "different raw", "raw field updated"); }); -QUnit.test('destroy by staff', assert => { - var user = Discourse.User.create({username: 'staff', staff: true}), - post = buildPost({user: user}); +QUnit.test("destroy by staff", assert => { + var user = Discourse.User.create({ username: "staff", staff: true }), + post = buildPost({ user: user }); post.destroy(user); - assert.present(post.get('deleted_at'), "it has a `deleted_at` field."); - assert.equal(post.get('deleted_by'), user, "it has the user in the `deleted_by` field"); + assert.present(post.get("deleted_at"), "it has a `deleted_at` field."); + assert.equal( + post.get("deleted_by"), + user, + "it has the user in the `deleted_by` field" + ); post.recover(); - assert.blank(post.get('deleted_at'), "it clears `deleted_at` when recovering"); - assert.blank(post.get('deleted_by'), "it clears `deleted_by` when recovering"); - + assert.blank( + post.get("deleted_at"), + "it clears `deleted_at` when recovering" + ); + assert.blank( + post.get("deleted_by"), + "it clears `deleted_by` when recovering" + ); }); -QUnit.test('destroy by non-staff', assert => { +QUnit.test("destroy by non-staff", assert => { var originalCooked = "this is the original cooked value", - user = Discourse.User.create({username: 'evil trout'}), - post = buildPost({user: user, cooked: originalCooked}); + user = Discourse.User.create({ username: "evil trout" }), + post = buildPost({ user: user, cooked: originalCooked }); return post.destroy(user).then(() => { - assert.ok(!post.get('can_delete'), "the post can't be deleted again in this session"); - assert.ok(post.get('cooked') !== originalCooked, "the cooked content changed"); - assert.equal(post.get('version'), 2, "the version number increased"); + assert.ok( + !post.get("can_delete"), + "the post can't be deleted again in this session" + ); + assert.ok( + post.get("cooked") !== originalCooked, + "the cooked content changed" + ); + assert.equal(post.get("version"), 2, "the version number increased"); }); }); diff --git a/test/javascripts/models/report-test.js.es6 b/test/javascripts/models/report-test.js.es6 index 6af1310573..96b05d94b7 100644 --- a/test/javascripts/models/report-test.js.es6 +++ b/test/javascripts/models/report-test.js.es6 @@ -6,7 +6,12 @@ function reportWithData(data) { return Report.create({ type: "topics", data: _.map(data, (val, index) => { - return { x: moment().subtract(index, "days").format("YYYY-MM-DD"), y: val }; + return { + x: moment() + .subtract(index, "days") + .format("YYYY-MM-DD"), + y: val + }; }) }); } @@ -16,11 +21,23 @@ QUnit.test("counts", assert => { assert.equal(report.get("todayCount"), 5); assert.equal(report.get("yesterdayCount"), 4); - assert.equal(report.valueFor(2, 4), 6, "adds the values for the given range of days, inclusive"); - assert.equal(report.get("lastSevenDaysCount"), 307, "sums 7 days excluding today"); + assert.equal( + report.valueFor(2, 4), + 6, + "adds the values for the given range of days, inclusive" + ); + assert.equal( + report.get("lastSevenDaysCount"), + 307, + "sums 7 days excluding today" + ); report.set("method", "average"); - assert.equal(report.valueFor(2, 4), 2, "averages the values for the given range of days"); + assert.equal( + report.valueFor(2, 4), + 2, + "averages the values for the given range of days" + ); }); QUnit.test("percentChangeString", assert => { @@ -29,32 +46,55 @@ QUnit.test("percentChangeString", assert => { assert.equal(report.percentChangeString(5, 8), "+60%", "value increased"); assert.equal(report.percentChangeString(8, 2), "-75%", "value decreased"); assert.equal(report.percentChangeString(8, 8), "0%", "value unchanged"); - assert.blank(report.percentChangeString(0, 8), "returns blank when previous value was 0"); + assert.blank( + report.percentChangeString(0, 8), + "returns blank when previous value was 0" + ); assert.equal(report.percentChangeString(8, 0), "-100%", "yesterday was 0"); - assert.blank(report.percentChangeString(0, 0), "returns blank when both were 0"); + assert.blank( + report.percentChangeString(0, 0), + "returns blank when both were 0" + ); }); QUnit.test("yesterdayCountTitle with valid values", assert => { - const title = reportWithData([6,8,5,2,1]).get("yesterdayCountTitle"); + const title = reportWithData([6, 8, 5, 2, 1]).get("yesterdayCountTitle"); assert.ok(title.indexOf("+60%") !== -1); assert.ok(title.match(/Was 5/)); }); QUnit.test("yesterdayCountTitle when two days ago was 0", assert => { - const title = reportWithData([6,8,0,2,1]).get("yesterdayCountTitle"); + const title = reportWithData([6, 8, 0, 2, 1]).get("yesterdayCountTitle"); assert.equal(title.indexOf("%"), -1); assert.ok(title.match(/Was 0/)); }); - QUnit.test("sevenDaysCountTitle", assert => { - const title = reportWithData([100,1,1,1,1,1,1,1,2,2,2,2,2,2,2,100,100]).get("sevenDaysCountTitle"); + const title = reportWithData([ + 100, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 100, + 100 + ]).get("sevenDaysCountTitle"); assert.ok(title.match(/-50%/)); assert.ok(title.match(/Was 14/)); }); QUnit.test("thirtyDaysCountTitle", assert => { - const report = reportWithData([5,5,5,5]); + const report = reportWithData([5, 5, 5, 5]); report.set("prev30Days", 10); const title = report.get("thirtyDaysCountTitle"); @@ -66,23 +106,23 @@ QUnit.test("sevenDaysTrend", assert => { let report; let trend; - report = reportWithData([0, 1,1,1,1,1,1,1, 1,1,1,1,1,1,1]); + report = reportWithData([0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]); trend = report.get("sevenDaysTrend"); assert.ok(trend === "no-change"); - report = reportWithData([0, 1,1,1,1,1,1,1, 0,0,0,0,0,0,0]); + report = reportWithData([0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0]); trend = report.get("sevenDaysTrend"); assert.ok(trend === "high-trending-up"); - report = reportWithData([0, 1,1,1,1,1,1,1, 1,1,1,1,1,1,0]); + report = reportWithData([0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0]); trend = report.get("sevenDaysTrend"); assert.ok(trend === "trending-up"); - report = reportWithData([0, 0,0,0,0,0,0,0, 1,1,1,1,1,1,1]); + report = reportWithData([0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1]); trend = report.get("sevenDaysTrend"); assert.ok(trend === "high-trending-down"); - report = reportWithData([0, 1,1,1,1,1,1,0, 1,1,1,1,1,1,1]); + report = reportWithData([0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1]); trend = report.get("sevenDaysTrend"); assert.ok(trend === "trending-down"); }); @@ -116,27 +156,187 @@ QUnit.test("thirtyDaysTrend", assert => { let report; let trend; - report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]); + report = reportWithData([ + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ]); report.set("prev30Days", 30); trend = report.get("thirtyDaysTrend"); assert.ok(trend === "no-change"); - report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]); + report = reportWithData([ + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ]); report.set("prev30Days", 0); trend = report.get("thirtyDaysTrend"); assert.ok(trend === "high-trending-up"); - report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]); + report = reportWithData([ + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ]); report.set("prev30Days", 25); trend = report.get("thirtyDaysTrend"); assert.ok(trend === "trending-up"); - report = reportWithData([0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); + report = reportWithData([ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ]); report.set("prev30Days", 60); trend = report.get("thirtyDaysTrend"); assert.ok(trend === "high-trending-down"); - report = reportWithData([0, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0]); + report = reportWithData([ + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 0 + ]); report.set("prev30Days", 35); trend = report.get("thirtyDaysTrend"); assert.ok(trend === "trending-down"); @@ -171,11 +371,11 @@ QUnit.test("small variation (-2/+2% change) is no-change", assert => { let report; let trend; - report = reportWithData([0, 1,1,1,1,1,1,0.9, 1,1,1,1,1,1,1]); + report = reportWithData([0, 1, 1, 1, 1, 1, 1, 0.9, 1, 1, 1, 1, 1, 1, 1]); trend = report.get("sevenDaysTrend"); assert.ok(trend === "no-change"); - report = reportWithData([0, 1,1,1,1,1,1,1.1, 1,1,1,1,1,1,1]); + report = reportWithData([0, 1, 1, 1, 1, 1, 1, 1.1, 1, 1, 1, 1, 1, 1, 1]); trend = report.get("sevenDaysTrend"); assert.ok(trend === "no-change"); }); diff --git a/test/javascripts/models/rest-model-test.js.es6 b/test/javascripts/models/rest-model-test.js.es6 index 031cb3b0c2..a19578a22b 100644 --- a/test/javascripts/models/rest-model-test.js.es6 +++ b/test/javascripts/models/rest-model-test.js.es6 @@ -1,9 +1,9 @@ -QUnit.module('rest-model'); +QUnit.module("rest-model"); -import createStore from 'helpers/create-store'; -import RestModel from 'discourse/models/rest'; +import createStore from "helpers/create-store"; +import RestModel from "discourse/models/rest"; -QUnit.test('munging', assert => { +QUnit.test("munging", assert => { const store = createStore(); const Grape = RestModel.extend(); Grape.reopenClass({ @@ -14,82 +14,81 @@ QUnit.test('munging', assert => { }); var g = Grape.create({ store, percent: 0.4 }); - assert.equal(g.get('inverse'), 0.6, 'it runs `munge` on `create`'); + assert.equal(g.get("inverse"), 0.6, "it runs `munge` on `create`"); }); -QUnit.test('update', assert => { +QUnit.test("update", assert => { const store = createStore(); - return store.find('widget', 123).then(function(widget) { - assert.equal(widget.get('name'), 'Trout Lure'); + return store.find("widget", 123).then(function(widget) { + assert.equal(widget.get("name"), "Trout Lure"); - assert.ok(!widget.get('isSaving')); - const promise = widget.update({ name: 'new name' }); - assert.ok(widget.get('isSaving')); + assert.ok(!widget.get("isSaving")); + const promise = widget.update({ name: "new name" }); + assert.ok(widget.get("isSaving")); promise.then(function() { - assert.ok(!widget.get('isSaving')); - assert.equal(widget.get('name'), 'new name'); + assert.ok(!widget.get("isSaving")); + assert.equal(widget.get("name"), "new name"); }); }); }); -QUnit.test('updating simultaneously', assert => { +QUnit.test("updating simultaneously", assert => { assert.expect(2); const store = createStore(); - return store.find('widget', 123).then(function(widget) { - - const firstPromise = widget.update({ name: 'new name' }); - const secondPromise = widget.update({ name: 'new name' }); + return store.find("widget", 123).then(function(widget) { + const firstPromise = widget.update({ name: "new name" }); + const secondPromise = widget.update({ name: "new name" }); firstPromise.then(function() { - assert.ok(true, 'the first promise succeeeds'); + assert.ok(true, "the first promise succeeeds"); }); secondPromise.catch(function() { - assert.ok(true, 'the second promise fails'); + assert.ok(true, "the second promise fails"); }); }); }); -QUnit.test('save new', assert => { +QUnit.test("save new", assert => { const store = createStore(); - const widget = store.createRecord('widget'); + const widget = store.createRecord("widget"); - assert.ok(widget.get('isNew'), 'it is a new record'); - assert.ok(!widget.get('isCreated'), 'it is not created'); - assert.ok(!widget.get('isSaving')); + assert.ok(widget.get("isNew"), "it is a new record"); + assert.ok(!widget.get("isCreated"), "it is not created"); + assert.ok(!widget.get("isSaving")); - const promise = widget.save({ name: 'Evil Widget' }); - assert.ok(widget.get('isSaving')); + const promise = widget.save({ name: "Evil Widget" }); + assert.ok(widget.get("isSaving")); return promise.then(function() { - assert.ok(!widget.get('isSaving')); - assert.ok(widget.get('id'), 'it has an id'); - assert.ok(widget.get('name'), 'Evil Widget'); - assert.ok(widget.get('isCreated'), 'it is created'); - assert.ok(!widget.get('isNew'), 'it is no longer new'); + assert.ok(!widget.get("isSaving")); + assert.ok(widget.get("id"), "it has an id"); + assert.ok(widget.get("name"), "Evil Widget"); + assert.ok(widget.get("isCreated"), "it is created"); + assert.ok(!widget.get("isNew"), "it is no longer new"); }); }); -QUnit.test('creating simultaneously', assert => { +QUnit.test("creating simultaneously", assert => { assert.expect(2); const store = createStore(); - const widget = store.createRecord('widget'); + const widget = store.createRecord("widget"); - const firstPromise = widget.save({ name: 'Evil Widget' }); - const secondPromise = widget.save({ name: 'Evil Widget' }); + const firstPromise = widget.save({ name: "Evil Widget" }); + const secondPromise = widget.save({ name: "Evil Widget" }); firstPromise.then(function() { - assert.ok(true, 'the first promise succeeeds'); + assert.ok(true, "the first promise succeeeds"); }); secondPromise.catch(function() { - assert.ok(true, 'the second promise fails'); + assert.ok(true, "the second promise fails"); }); }); -QUnit.test('destroyRecord', assert => { +QUnit.test("destroyRecord", assert => { const store = createStore(); - return store.find('widget', 123).then(function(widget) { + return store.find("widget", 123).then(function(widget) { widget.destroyRecord().then(function(result) { assert.ok(result); }); diff --git a/test/javascripts/models/result-set-test.js.es6 b/test/javascripts/models/result-set-test.js.es6 index 2912b8ddb8..1f819b317e 100644 --- a/test/javascripts/models/result-set-test.js.es6 +++ b/test/javascripts/models/result-set-test.js.es6 @@ -1,49 +1,53 @@ -QUnit.module('result-set'); +QUnit.module("result-set"); -import ResultSet from 'discourse/models/result-set'; -import createStore from 'helpers/create-store'; +import ResultSet from "discourse/models/result-set"; +import createStore from "helpers/create-store"; -QUnit.test('defaults', assert => { +QUnit.test("defaults", assert => { const rs = ResultSet.create({ content: [] }); - assert.equal(rs.get('length'), 0); - assert.equal(rs.get('totalRows'), 0); - assert.ok(!rs.get('loadMoreUrl')); - assert.ok(!rs.get('loading')); - assert.ok(!rs.get('loadingMore')); - assert.ok(!rs.get('refreshing')); + assert.equal(rs.get("length"), 0); + assert.equal(rs.get("totalRows"), 0); + assert.ok(!rs.get("loadMoreUrl")); + assert.ok(!rs.get("loading")); + assert.ok(!rs.get("loadingMore")); + assert.ok(!rs.get("refreshing")); }); -QUnit.test('pagination support', assert => { +QUnit.test("pagination support", assert => { const store = createStore(); - return store.findAll('widget').then(function(rs) { - assert.equal(rs.get('length'), 2); - assert.equal(rs.get('totalRows'), 4); - assert.ok(rs.get('loadMoreUrl'), 'has a url to load more'); - assert.ok(!rs.get('loadingMore'), 'it is not loading more'); - assert.ok(rs.get('canLoadMore')); + return store.findAll("widget").then(function(rs) { + assert.equal(rs.get("length"), 2); + assert.equal(rs.get("totalRows"), 4); + assert.ok(rs.get("loadMoreUrl"), "has a url to load more"); + assert.ok(!rs.get("loadingMore"), "it is not loading more"); + assert.ok(rs.get("canLoadMore")); const promise = rs.loadMore(); - assert.ok(rs.get('loadingMore'), 'it is loading more'); + assert.ok(rs.get("loadingMore"), "it is loading more"); promise.then(function() { - assert.ok(!rs.get('loadingMore'), 'it finished loading more'); - assert.equal(rs.get('length'), 4); - assert.ok(!rs.get('loadMoreUrl')); - assert.ok(!rs.get('canLoadMore')); + assert.ok(!rs.get("loadingMore"), "it finished loading more"); + assert.equal(rs.get("length"), 4); + assert.ok(!rs.get("loadMoreUrl")); + assert.ok(!rs.get("canLoadMore")); }); }); }); -QUnit.test('refresh support', assert => { +QUnit.test("refresh support", assert => { const store = createStore(); - return store.findAll('widget').then(function(rs) { - assert.equal(rs.get('refreshUrl'), '/widgets?refresh=true', 'it has the refresh url'); + return store.findAll("widget").then(function(rs) { + assert.equal( + rs.get("refreshUrl"), + "/widgets?refresh=true", + "it has the refresh url" + ); const promise = rs.refresh(); - assert.ok(rs.get('refreshing'), 'it is refreshing'); + assert.ok(rs.get("refreshing"), "it is refreshing"); promise.then(function() { - assert.ok(!rs.get('refreshing'), 'it is finished refreshing'); + assert.ok(!rs.get("refreshing"), "it is finished refreshing"); }); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/models/session-test.js.es6 b/test/javascripts/models/session-test.js.es6 index ff330fd77f..2208c91a01 100644 --- a/test/javascripts/models/session-test.js.es6 +++ b/test/javascripts/models/session-test.js.es6 @@ -2,7 +2,11 @@ import Session from "discourse/models/session"; QUnit.module("model:session"); -QUnit.test('highestSeenByTopic', assert => { +QUnit.test("highestSeenByTopic", assert => { const session = Session.current(); - assert.deepEqual(session.get('highestSeenByTopic'), {}, "by default it returns an empty object"); -}); \ No newline at end of file + assert.deepEqual( + session.get("highestSeenByTopic"), + {}, + "by default it returns an empty object" + ); +}); diff --git a/test/javascripts/models/site-test.js.es6 b/test/javascripts/models/site-test.js.es6 index bce69122dc..d240abe45b 100644 --- a/test/javascripts/models/site-test.js.es6 +++ b/test/javascripts/models/site-test.js.es6 @@ -1,48 +1,69 @@ -import createStore from 'helpers/create-store'; +import createStore from "helpers/create-store"; QUnit.module("model:site"); -QUnit.test('create', assert => { - assert.ok(Discourse.Site.create(), 'it can create with no parameters'); +QUnit.test("create", assert => { + assert.ok(Discourse.Site.create(), "it can create with no parameters"); }); -QUnit.test('instance', assert => { +QUnit.test("instance", assert => { const site = Discourse.Site.current(); assert.present(site, "We have a current site singleton"); - assert.present(site.get('categories'), "The instance has a list of categories"); - assert.present(site.get('flagTypes'), "The instance has a list of flag types"); - assert.present(site.get('trustLevels'), "The instance has a list of trust levels"); - + assert.present( + site.get("categories"), + "The instance has a list of categories" + ); + assert.present( + site.get("flagTypes"), + "The instance has a list of flag types" + ); + assert.present( + site.get("trustLevels"), + "The instance has a list of trust levels" + ); }); -QUnit.test('create categories', assert => { +QUnit.test("create categories", assert => { const store = createStore(); - const site = store.createRecord('site', { - categories: [{ id: 1234, name: 'Test'}, - { id: 3456, name: 'Test Subcategory', parent_category_id: 1234}, - { id: 3458, name: 'Invalid Subcategory', parent_category_id: 6666}] + const site = store.createRecord("site", { + categories: [ + { id: 1234, name: "Test" }, + { id: 3456, name: "Test Subcategory", parent_category_id: 1234 }, + { id: 3458, name: "Invalid Subcategory", parent_category_id: 6666 } + ] }); - const categories = site.get('categories'); - site.get('sortedCategories'); + const categories = site.get("categories"); + site.get("sortedCategories"); assert.present(categories, "The categories are present"); assert.equal(categories.length, 3, "it loaded all three categories"); - const parent = categories.findBy('id', 1234); + const parent = categories.findBy("id", 1234); assert.present(parent, "it loaded the parent category"); - assert.blank(parent.get('parentCategory'), 'it has no parent category'); + assert.blank(parent.get("parentCategory"), "it has no parent category"); - const subcategory = categories.findBy('id', 3456); + const subcategory = categories.findBy("id", 3456); assert.present(subcategory, "it loaded the subcategory"); - assert.equal(subcategory.get('parentCategory'), parent, "it has associated the child with the parent"); + assert.equal( + subcategory.get("parentCategory"), + parent, + "it has associated the child with the parent" + ); // remove invalid category and child categories.removeObject(categories[2]); categories.removeObject(categories[1]); - assert.equal(categories.length, site.get('categoriesByCount').length, "categories by count should change on removal"); - assert.equal(categories.length, site.get('sortedCategories').length, "sorted categories should change on removal"); - -}); \ No newline at end of file + assert.equal( + categories.length, + site.get("categoriesByCount").length, + "categories by count should change on removal" + ); + assert.equal( + categories.length, + site.get("sortedCategories").length, + "sorted categories should change on removal" + ); +}); diff --git a/test/javascripts/models/staff-action-log-test.js.es6 b/test/javascripts/models/staff-action-log-test.js.es6 index 2e9756ff9e..22ef0192db 100644 --- a/test/javascripts/models/staff-action-log-test.js.es6 +++ b/test/javascripts/models/staff-action-log-test.js.es6 @@ -1,7 +1,7 @@ -import StaffActionLog from 'admin/models/staff-action-log'; +import StaffActionLog from "admin/models/staff-action-log"; QUnit.module("StaffActionLog"); QUnit.test("create", assert => { assert.ok(StaffActionLog.create(), "it can be created without arguments"); -}); \ No newline at end of file +}); diff --git a/test/javascripts/models/store-test.js.es6 b/test/javascripts/models/store-test.js.es6 index e62d77dc4e..a9ce8e2c59 100644 --- a/test/javascripts/models/store-test.js.es6 +++ b/test/javascripts/models/store-test.js.es6 @@ -1,169 +1,190 @@ -QUnit.module('service:store'); +QUnit.module("service:store"); -import createStore from 'helpers/create-store'; +import createStore from "helpers/create-store"; -QUnit.test('createRecord', assert => { +QUnit.test("createRecord", assert => { const store = createStore(); - const widget = store.createRecord('widget', {id: 111, name: 'hello'}); + const widget = store.createRecord("widget", { id: 111, name: "hello" }); - assert.ok(!widget.get('isNew'), 'it is not a new record'); - assert.equal(widget.get('name'), 'hello'); - assert.equal(widget.get('id'), 111); + assert.ok(!widget.get("isNew"), "it is not a new record"); + assert.equal(widget.get("name"), "hello"); + assert.equal(widget.get("id"), 111); }); -QUnit.test('createRecord without an `id`', assert => { +QUnit.test("createRecord without an `id`", assert => { const store = createStore(); - const widget = store.createRecord('widget', {name: 'hello'}); + const widget = store.createRecord("widget", { name: "hello" }); - assert.ok(widget.get('isNew'), 'it is a new record'); - assert.ok(!widget.get('id'), 'there is no id'); + assert.ok(widget.get("isNew"), "it is a new record"); + assert.ok(!widget.get("id"), "there is no id"); }); QUnit.test("createRecord doesn't modify the input `id` field", assert => { const store = createStore(); - const widget = store.createRecord('widget', {id: 1, name: 'hello'}); + const widget = store.createRecord("widget", { id: 1, name: "hello" }); - const obj = { id: 1, name: 'something' }; + const obj = { id: 1, name: "something" }; - const other = store.createRecord('widget', obj); - assert.equal(widget, other, 'returns the same record'); - assert.equal(widget.name, 'something', 'it updates the properties'); - assert.equal(obj.id, 1, 'it does not remove the id from the input'); + const other = store.createRecord("widget", obj); + assert.equal(widget, other, "returns the same record"); + assert.equal(widget.name, "something", "it updates the properties"); + assert.equal(obj.id, 1, "it does not remove the id from the input"); }); -QUnit.test('createRecord without attributes', assert => { +QUnit.test("createRecord without attributes", assert => { const store = createStore(); - const widget = store.createRecord('widget'); + const widget = store.createRecord("widget"); - assert.ok(!widget.get('id'), 'there is no id'); - assert.ok(widget.get('isNew'), 'it is a new record'); + assert.ok(!widget.get("id"), "there is no id"); + assert.ok(widget.get("isNew"), "it is a new record"); }); -QUnit.test('createRecord with a record as attributes returns that record from the map', assert => { - const store = createStore(); - const widget = store.createRecord('widget', {id: 33}); - const secondWidget = store.createRecord('widget', {id: 33}); +QUnit.test( + "createRecord with a record as attributes returns that record from the map", + assert => { + const store = createStore(); + const widget = store.createRecord("widget", { id: 33 }); + const secondWidget = store.createRecord("widget", { id: 33 }); - assert.equal(widget, secondWidget, 'they should be the same'); -}); + assert.equal(widget, secondWidget, "they should be the same"); + } +); -QUnit.test('find', assert => { +QUnit.test("find", assert => { const store = createStore(); - return store.find('widget', 123).then(function(w) { - assert.equal(w.get('name'), 'Trout Lure'); - assert.equal(w.get('id'), 123); - assert.ok(!w.get('isNew'), 'found records are not new'); - assert.equal(w.get('extras.hello'), 'world', "extra attributes are set"); + return store.find("widget", 123).then(function(w) { + assert.equal(w.get("name"), "Trout Lure"); + assert.equal(w.get("id"), 123); + assert.ok(!w.get("isNew"), "found records are not new"); + assert.equal(w.get("extras.hello"), "world", "extra attributes are set"); // A second find by id returns the same object - store.find('widget', 123).then(function(w2) { + store.find("widget", 123).then(function(w2) { assert.equal(w, w2); - assert.equal(w.get('extras.hello'), 'world', "extra attributes are set"); + assert.equal(w.get("extras.hello"), "world", "extra attributes are set"); }); }); }); -QUnit.test('find with object id', assert => { +QUnit.test("find with object id", assert => { const store = createStore(); - return store.find('widget', {id: 123}).then(function(w) { - assert.equal(w.get('firstObject.name'), 'Trout Lure'); + return store.find("widget", { id: 123 }).then(function(w) { + assert.equal(w.get("firstObject.name"), "Trout Lure"); }); }); -QUnit.test('find with query param', assert => { +QUnit.test("find with query param", assert => { const store = createStore(); - return store.find('widget', {name: 'Trout Lure'}).then(function(w) { - assert.equal(w.get('firstObject.id'), 123); + return store.find("widget", { name: "Trout Lure" }).then(function(w) { + assert.equal(w.get("firstObject.id"), 123); }); }); -QUnit.test('findStale with no stale results', (assert) => { +QUnit.test("findStale with no stale results", assert => { const store = createStore(); - const stale = store.findStale('widget', {name: 'Trout Lure'}); + const stale = store.findStale("widget", { name: "Trout Lure" }); - assert.ok(!stale.hasResults, 'there are no stale results'); - assert.ok(!stale.results, 'results are present'); + assert.ok(!stale.hasResults, "there are no stale results"); + assert.ok(!stale.results, "results are present"); return stale.refresh().then(function(w) { - assert.equal(w.get('firstObject.id'), 123, 'a `refresh()` method provides results for stale'); + assert.equal( + w.get("firstObject.id"), + 123, + "a `refresh()` method provides results for stale" + ); }); }); -QUnit.test('update', assert => { +QUnit.test("update", assert => { const store = createStore(); - return store.update('widget', 123, {name: 'hello'}).then(function(result) { + return store.update("widget", 123, { name: "hello" }).then(function(result) { assert.ok(result); }); }); -QUnit.test('update with a multi world name', function(assert) { +QUnit.test("update with a multi world name", function(assert) { const store = createStore(); - return store.update('cool-thing', 123, {name: 'hello'}).then(function(result) { - assert.ok(result); - assert.equal(result.payload.name, 'hello'); + return store + .update("cool-thing", 123, { name: "hello" }) + .then(function(result) { + assert.ok(result); + assert.equal(result.payload.name, "hello"); + }); +}); + +QUnit.test("findAll", assert => { + const store = createStore(); + return store.findAll("widget").then(function(result) { + assert.equal(result.get("length"), 2); + const w = result.findBy("id", 124); + assert.ok(!w.get("isNew"), "found records are not new"); + assert.equal(w.get("name"), "Evil Repellant"); }); }); -QUnit.test('findAll', assert => { +QUnit.test("destroyRecord", function(assert) { const store = createStore(); - return store.findAll('widget').then(function(result) { - assert.equal(result.get('length'), 2); - const w = result.findBy('id', 124); - assert.ok(!w.get('isNew'), 'found records are not new'); - assert.equal(w.get('name'), 'Evil Repellant'); - }); -}); - -QUnit.test('destroyRecord', function(assert) { - const store = createStore(); - return store.find('widget', 123).then(function(w) { - store.destroyRecord('widget', w).then(function(result) { + return store.find("widget", 123).then(function(w) { + store.destroyRecord("widget", w).then(function(result) { assert.ok(result); }); }); }); -QUnit.test('destroyRecord when new', function(assert) { +QUnit.test("destroyRecord when new", function(assert) { const store = createStore(); - const w = store.createRecord('widget', {name: 'hello'}); - store.destroyRecord('widget', w).then(function(result) { + const w = store.createRecord("widget", { name: "hello" }); + store.destroyRecord("widget", w).then(function(result) { assert.ok(result); }); }); -QUnit.test('find embedded', function(assert) { +QUnit.test("find embedded", function(assert) { const store = createStore(); - return store.find('fruit', 2).then(function(f) { - assert.ok(f.get('farmer'), 'it has the embedded object'); + return store.find("fruit", 2).then(function(f) { + assert.ok(f.get("farmer"), "it has the embedded object"); - const fruitCols = f.get('colors'); + const fruitCols = f.get("colors"); assert.equal(fruitCols.length, 2); - assert.equal(fruitCols[0].get('id'), 1); - assert.equal(fruitCols[1].get('id'), 2); + assert.equal(fruitCols[0].get("id"), 1); + assert.equal(fruitCols[1].get("id"), 2); - assert.ok(f.get('category'), 'categories are found automatically'); + assert.ok(f.get("category"), "categories are found automatically"); }); }); -QUnit.test('meta types', function(assert) { +QUnit.test("meta types", function(assert) { const store = createStore(); - return store.find('barn', 1).then(function(f) { - assert.equal(f.get('owner.name'), 'Old MacDonald', 'it has the embedded farmer'); + return store.find("barn", 1).then(function(f) { + assert.equal( + f.get("owner.name"), + "Old MacDonald", + "it has the embedded farmer" + ); }); }); -QUnit.test('findAll embedded', function(assert) { +QUnit.test("findAll embedded", function(assert) { const store = createStore(); - return store.findAll('fruit').then(function(fruits) { - assert.equal(fruits.objectAt(0).get('farmer.name'), 'Old MacDonald'); - assert.equal(fruits.objectAt(0).get('farmer'), fruits.objectAt(1).get('farmer'), 'points at the same object'); - assert.equal(fruits.get('extras.hello'), 'world', 'it can supply extra information'); + return store.findAll("fruit").then(function(fruits) { + assert.equal(fruits.objectAt(0).get("farmer.name"), "Old MacDonald"); + assert.equal( + fruits.objectAt(0).get("farmer"), + fruits.objectAt(1).get("farmer"), + "points at the same object" + ); + assert.equal( + fruits.get("extras.hello"), + "world", + "it can supply extra information" + ); - const fruitCols = fruits.objectAt(0).get('colors'); + const fruitCols = fruits.objectAt(0).get("colors"); assert.equal(fruitCols.length, 2); - assert.equal(fruitCols[0].get('id'), 1); - assert.equal(fruitCols[1].get('id'), 2); + assert.equal(fruitCols[0].get("id"), 1); + assert.equal(fruitCols[1].get("id"), 2); - assert.equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker'); + assert.equal(fruits.objectAt(2).get("farmer.name"), "Luke Skywalker"); }); }); diff --git a/test/javascripts/models/topic-details-test.js.es6 b/test/javascripts/models/topic-details-test.js.es6 index bc004748a1..35534faf69 100644 --- a/test/javascripts/models/topic-details-test.js.es6 +++ b/test/javascripts/models/topic-details-test.js.es6 @@ -1,26 +1,29 @@ QUnit.module("model:topic-details"); -import Topic from 'discourse/models/topic'; +import Topic from "discourse/models/topic"; var buildDetails = function(id) { - var topic = Topic.create({id: id}); - return topic.get('details'); + var topic = Topic.create({ id: id }); + return topic.get("details"); }; -QUnit.test('defaults', assert => { +QUnit.test("defaults", assert => { var details = buildDetails(1234); assert.present(details, "the details are present by default"); - assert.ok(!details.get('loaded'), "details are not loaded by default"); + assert.ok(!details.get("loaded"), "details are not loaded by default"); }); -QUnit.test('updateFromJson', assert => { +QUnit.test("updateFromJson", assert => { var details = buildDetails(1234); details.updateFromJson({ - allowed_users: [{username: 'eviltrout'}] + allowed_users: [{ username: "eviltrout" }] }); - assert.equal(details.get('allowed_users.length'), 1, 'it loaded the allowed users'); - assert.containsInstance(details.get('allowed_users'), Discourse.User); - + assert.equal( + details.get("allowed_users.length"), + 1, + "it loaded the allowed users" + ); + assert.containsInstance(details.get("allowed_users"), Discourse.User); }); diff --git a/test/javascripts/models/topic-test.js.es6 b/test/javascripts/models/topic-test.js.es6 index 7c33bceff0..de606ef6b3 100644 --- a/test/javascripts/models/topic-test.js.es6 +++ b/test/javascripts/models/topic-test.js.es6 @@ -1,58 +1,75 @@ -import { IMAGE_VERSION as v } from 'pretty-text/emoji'; +import { IMAGE_VERSION as v } from "pretty-text/emoji"; QUnit.module("model:topic"); -import Topic from 'discourse/models/topic'; +import Topic from "discourse/models/topic"; QUnit.test("defaults", assert => { const topic = Topic.create({ id: 1234 }); - assert.blank(topic.get('deleted_at'), 'deleted_at defaults to blank'); - assert.blank(topic.get('deleted_by'), 'deleted_by defaults to blank'); + assert.blank(topic.get("deleted_at"), "deleted_at defaults to blank"); + assert.blank(topic.get("deleted_by"), "deleted_by defaults to blank"); }); QUnit.test("visited", assert => { - const topic = Topic.create({ highest_post_number: 2, last_read_post_number: 1 }); + const topic = Topic.create({ + highest_post_number: 2, + last_read_post_number: 1 + }); - assert.not(topic.get("visited"), "not visited unless we've read all the posts"); + assert.not( + topic.get("visited"), + "not visited unless we've read all the posts" + ); topic.set("last_read_post_number", 2); assert.ok(topic.get("visited"), "is visited once we've read all the posts"); topic.set("last_read_post_number", 3); - assert.ok(topic.get("visited"), "is visited if we've read all the posts and some are deleted at the end"); + assert.ok( + topic.get("visited"), + "is visited if we've read all the posts and some are deleted at the end" + ); }); -QUnit.test('has details', assert => { +QUnit.test("has details", assert => { const topic = Topic.create({ id: 1234 }); - const topicDetails = topic.get('details'); + const topicDetails = topic.get("details"); assert.present(topicDetails, "a topic has topicDetails after we create it"); - assert.equal(topicDetails.get('topic'), topic, "the topicDetails has a reference back to the topic"); + assert.equal( + topicDetails.get("topic"), + topic, + "the topicDetails has a reference back to the topic" + ); }); -QUnit.test('has a postStream', assert => { +QUnit.test("has a postStream", assert => { const topic = Topic.create({ id: 1234 }); - const postStream = topic.get('postStream'); + const postStream = topic.get("postStream"); assert.present(postStream, "a topic has a postStream after we create it"); - assert.equal(postStream.get('topic'), topic, "the postStream has a reference back to the topic"); + assert.equal( + postStream.get("topic"), + topic, + "the postStream has a reference back to the topic" + ); }); -QUnit.test('has suggestedTopics', assert => { +QUnit.test("has suggestedTopics", assert => { const topic = Topic.create({ suggested_topics: [{ id: 1 }, { id: 2 }] }); - const suggestedTopics = topic.get('suggestedTopics'); + const suggestedTopics = topic.get("suggestedTopics"); - assert.equal(suggestedTopics.length, 2, 'it loaded the suggested_topics'); + assert.equal(suggestedTopics.length, 2, "it loaded the suggested_topics"); assert.containsInstance(suggestedTopics, Topic); }); -QUnit.test('category relationship', assert => { +QUnit.test("category relationship", assert => { // It finds the category by id const category = Discourse.Category.list()[0]; - const topic = Topic.create({ id: 1111, category_id: category.get('id') }); + const topic = Topic.create({ id: 1111, category_id: category.get("id") }); - assert.equal(topic.get('category'), category); + assert.equal(topic.get("category"), category); }); QUnit.test("updateFromJson", assert => { @@ -60,57 +77,78 @@ QUnit.test("updateFromJson", assert => { const category = Discourse.Category.list()[0]; topic.updateFromJson({ - post_stream: [1,2,3], - details: { hello: 'world' }, - cool: 'property', - category_id: category.get('id') + post_stream: [1, 2, 3], + details: { hello: "world" }, + cool: "property", + category_id: category.get("id") }); - assert.blank(topic.get('post_stream'), "it does not update post_stream"); - assert.equal(topic.get('details.hello'), 'world', 'it updates the details'); - assert.equal(topic.get('cool'), "property", "it updates other properties"); - assert.equal(topic.get('category'), category); + assert.blank(topic.get("post_stream"), "it does not update post_stream"); + assert.equal(topic.get("details.hello"), "world", "it updates the details"); + assert.equal(topic.get("cool"), "property", "it updates other properties"); + assert.equal(topic.get("category"), category); }); QUnit.test("destroy", assert => { - const user = Discourse.User.create({ username: 'eviltrout' }); + const user = Discourse.User.create({ username: "eviltrout" }); const topic = Topic.create({ id: 1234 }); topic.destroy(user); - assert.present(topic.get('deleted_at'), 'deleted at is set'); - assert.equal(topic.get('deleted_by'), user, 'deleted by is set'); + assert.present(topic.get("deleted_at"), "deleted at is set"); + assert.equal(topic.get("deleted_by"), user, "deleted by is set"); }); QUnit.test("recover", assert => { - const user = Discourse.User.create({ username: 'eviltrout' }); - const topic = Topic.create({ id: 1234, deleted_at: new Date(), deleted_by: user }); + const user = Discourse.User.create({ username: "eviltrout" }); + const topic = Topic.create({ + id: 1234, + deleted_at: new Date(), + deleted_by: user + }); topic.recover(); - assert.blank(topic.get('deleted_at'), "it clears deleted_at"); - assert.blank(topic.get('deleted_by'), "it clears deleted_by"); + assert.blank(topic.get("deleted_at"), "it clears deleted_at"); + assert.blank(topic.get("deleted_by"), "it clears deleted_by"); }); -QUnit.test('fancyTitle', assert => { - const topic = Topic.create({ fancy_title: ":smile: with all :) the emojis :pear::peach:" }); +QUnit.test("fancyTitle", assert => { + const topic = Topic.create({ + fancy_title: ":smile: with all :) the emojis :pear::peach:" + }); - assert.equal(topic.get('fancyTitle'), - `smile with all slight_smile the emojis pearpeach`, - "supports emojis"); + assert.equal( + topic.get("fancyTitle"), + `smile with all slight_smile the emojis pearpeach`, + "supports emojis" + ); }); -QUnit.test('fancyTitle direction', assert => { +QUnit.test("fancyTitle direction", assert => { const rtlTopic = Topic.create({ fancy_title: "هذا اختبار" }); - const ltrTopic = Topic.create({ fancy_title: "This is a test"}); + const ltrTopic = Topic.create({ fancy_title: "This is a test" }); Discourse.SiteSettings.support_mixed_text_direction = true; - assert.equal(rtlTopic.get('fancyTitle'), `هذا اختبار`, "sets the dir-span to rtl"); - assert.equal(ltrTopic.get('fancyTitle'), `This is a test`, "sets the dir-span to ltr"); + assert.equal( + rtlTopic.get("fancyTitle"), + `هذا اختبار`, + "sets the dir-span to rtl" + ); + assert.equal( + ltrTopic.get("fancyTitle"), + `This is a test`, + "sets the dir-span to ltr" + ); }); -QUnit.test('excerpt', assert => { - const topic = Topic.create({ excerpt: "This is a test topic :smile:", pinned: true }); +QUnit.test("excerpt", assert => { + const topic = Topic.create({ + excerpt: "This is a test topic :smile:", + pinned: true + }); - assert.equal(topic.get('escapedExcerpt'), - `This is a test topic smile`, - "supports emojis"); + assert.equal( + topic.get("escapedExcerpt"), + `This is a test topic smile`, + "supports emojis" + ); }); diff --git a/test/javascripts/models/topic-tracking-state-test.js.es6 b/test/javascripts/models/topic-tracking-state-test.js.es6 index 3c184cb759..5799b4f116 100644 --- a/test/javascripts/models/topic-tracking-state-test.js.es6 +++ b/test/javascripts/models/topic-tracking-state-test.js.es6 @@ -1,50 +1,92 @@ -import TopicTrackingState from 'discourse/models/topic-tracking-state'; -import createStore from 'helpers/create-store'; +import TopicTrackingState from "discourse/models/topic-tracking-state"; +import createStore from "helpers/create-store"; QUnit.module("model:topic-tracking-state"); -QUnit.test("sync", function (assert) { +QUnit.test("sync", function(assert) { const state = TopicTrackingState.create(); - state.states["t111"] = {last_read_post_number: null}; + state.states["t111"] = { last_read_post_number: null }; state.updateSeen(111, 7); - const list = {topics: [{ - highest_post_number: null, - id: 111, - unread: 10, - new_posts: 10 - }]}; + const list = { + topics: [ + { + highest_post_number: null, + id: 111, + unread: 10, + new_posts: 10 + } + ] + }; state.sync(list, "new"); - assert.equal(list.topics.length, 0, "expect new topic to be removed as it was seen"); + assert.equal( + list.topics.length, + 0, + "expect new topic to be removed as it was seen" + ); }); -QUnit.test("subscribe to category", function(assert){ - +QUnit.test("subscribe to category", function(assert) { const store = createStore(); - const darth = store.createRecord('category', {id: 1, slug: 'darth'}), - luke = store.createRecord('category', {id: 2, slug: 'luke', parentCategory: darth}), + const darth = store.createRecord("category", { id: 1, slug: "darth" }), + luke = store.createRecord("category", { + id: 2, + slug: "luke", + parentCategory: darth + }), categoryList = [darth, luke]; - sandbox.stub(Discourse.Category, 'list').returns(categoryList); - + sandbox.stub(Discourse.Category, "list").returns(categoryList); const state = TopicTrackingState.create(); - state.trackIncoming('c/darth/l/latest'); + state.trackIncoming("c/darth/l/latest"); - state.notify({message_type: 'new_topic', topic_id: 1, payload: {category_id: 2, topic_id: 1}}); - state.notify({message_type: 'new_topic', topic_id: 2, payload: {category_id: 3, topic_id: 2}}); - state.notify({message_type: 'new_topic', topic_id: 3, payload: {category_id: 1, topic_id: 3}}); + state.notify({ + message_type: "new_topic", + topic_id: 1, + payload: { category_id: 2, topic_id: 1 } + }); + state.notify({ + message_type: "new_topic", + topic_id: 2, + payload: { category_id: 3, topic_id: 2 } + }); + state.notify({ + message_type: "new_topic", + topic_id: 3, + payload: { category_id: 1, topic_id: 3 } + }); - assert.equal(state.get("incomingCount"), 2, "expect to properly track incoming for category"); + assert.equal( + state.get("incomingCount"), + 2, + "expect to properly track incoming for category" + ); state.resetTracking(); - state.trackIncoming('c/darth/luke/l/latest'); + state.trackIncoming("c/darth/luke/l/latest"); - state.notify({message_type: 'new_topic', topic_id: 1, payload: {category_id: 2, topic_id: 1}}); - state.notify({message_type: 'new_topic', topic_id: 2, payload: {category_id: 3, topic_id: 2}}); - state.notify({message_type: 'new_topic', topic_id: 3, payload: {category_id: 1, topic_id: 3}}); + state.notify({ + message_type: "new_topic", + topic_id: 1, + payload: { category_id: 2, topic_id: 1 } + }); + state.notify({ + message_type: "new_topic", + topic_id: 2, + payload: { category_id: 3, topic_id: 2 } + }); + state.notify({ + message_type: "new_topic", + topic_id: 3, + payload: { category_id: 1, topic_id: 3 } + }); - assert.equal(state.get("incomingCount"), 1, "expect to properly track incoming for subcategory"); -}); \ No newline at end of file + assert.equal( + state.get("incomingCount"), + 1, + "expect to properly track incoming for subcategory" + ); +}); diff --git a/test/javascripts/models/user-action-test.js.es6 b/test/javascripts/models/user-action-test.js.es6 index 93a988acac..c39d7d828f 100644 --- a/test/javascripts/models/user-action-test.js.es6 +++ b/test/javascripts/models/user-action-test.js.es6 @@ -7,12 +7,14 @@ QUnit.test("collapsing likes", assert => { topic_id: 1, user_id: 1, post_number: 1 - }), Discourse.UserAction.create({ + }), + Discourse.UserAction.create({ action_type: Discourse.UserAction.TYPES.edits, topic_id: 2, user_id: 1, post_number: 1 - }), Discourse.UserAction.create({ + }), + Discourse.UserAction.create({ action_type: Discourse.UserAction.TYPES.likes_given, topic_id: 1, user_id: 2, @@ -22,6 +24,6 @@ QUnit.test("collapsing likes", assert => { assert.equal(actions.length, 2); - assert.equal(actions[0].get('children.length'), 1); - assert.equal(actions[0].get('children')[0].items.length, 2); -}); \ No newline at end of file + assert.equal(actions[0].get("children.length"), 1); + assert.equal(actions[0].get("children")[0].items.length, 2); +}); diff --git a/test/javascripts/models/user-badge-test.js.es6 b/test/javascripts/models/user-badge-test.js.es6 index 0794c2f92c..2a0fdd9d6f 100644 --- a/test/javascripts/models/user-badge-test.js.es6 +++ b/test/javascripts/models/user-badge-test.js.es6 @@ -1,42 +1,60 @@ -import UserBadge from 'discourse/models/user-badge'; -import badgeFixtures from 'fixtures/user-badges'; +import UserBadge from "discourse/models/user-badge"; +import badgeFixtures from "fixtures/user-badges"; QUnit.module("model:user-badge"); -QUnit.test('createFromJson single', assert => { - const userBadge = UserBadge.createFromJson(badgeFixtures['/user_badges']); +QUnit.test("createFromJson single", assert => { + const userBadge = UserBadge.createFromJson(badgeFixtures["/user_badges"]); assert.ok(!Array.isArray(userBadge), "does not return an array"); - assert.equal(userBadge.get('badge.name'), "Badge 2", "badge reference is set"); - assert.equal(userBadge.get('badge.badge_type.name'), "Silver 2", "badge.badge_type reference is set"); - assert.equal(userBadge.get('granted_by.username'), "anne3", "granted_by reference is set"); + assert.equal( + userBadge.get("badge.name"), + "Badge 2", + "badge reference is set" + ); + assert.equal( + userBadge.get("badge.badge_type.name"), + "Silver 2", + "badge.badge_type reference is set" + ); + assert.equal( + userBadge.get("granted_by.username"), + "anne3", + "granted_by reference is set" + ); }); -QUnit.test('createFromJson array', assert => { - const userBadges = UserBadge.createFromJson(badgeFixtures['/user-badges/:username']); +QUnit.test("createFromJson array", assert => { + const userBadges = UserBadge.createFromJson( + badgeFixtures["/user-badges/:username"] + ); assert.ok(Array.isArray(userBadges), "returns an array"); - assert.equal(userBadges[0].get('granted_by'), null, "granted_by reference is not set when null"); + assert.equal( + userBadges[0].get("granted_by"), + null, + "granted_by reference is not set when null" + ); }); -QUnit.test('findByUsername', assert => { +QUnit.test("findByUsername", assert => { return UserBadge.findByUsername("anne3").then(function(badges) { assert.ok(Array.isArray(badges), "returns an array"); }); }); -QUnit.test('findByBadgeId', assert => { +QUnit.test("findByBadgeId", assert => { return UserBadge.findByBadgeId(880).then(function(badges) { assert.ok(Array.isArray(badges), "returns an array"); }); }); -QUnit.test('grant', assert => { +QUnit.test("grant", assert => { return UserBadge.grant(1, "username").then(function(userBadge) { assert.ok(!Array.isArray(userBadge), "does not return an array"); }); }); -QUnit.test('revoke', assert => { +QUnit.test("revoke", assert => { assert.expect(0); - const userBadge = UserBadge.create({id: 1}); + const userBadge = UserBadge.create({ id: 1 }); return userBadge.revoke(); -}); \ No newline at end of file +}); diff --git a/test/javascripts/models/user-stream-test.js.es6 b/test/javascripts/models/user-stream-test.js.es6 index 272dae3d33..d8be562c7a 100644 --- a/test/javascripts/models/user-stream-test.js.es6 +++ b/test/javascripts/models/user-stream-test.js.es6 @@ -1,30 +1,31 @@ QUnit.module("Discourse.UserStream"); -QUnit.test('basics', assert =>{ - var user = Discourse.User.create({id: 1, username: 'eviltrout'}); - var stream = user.get('stream'); +QUnit.test("basics", assert => { + var user = Discourse.User.create({ id: 1, username: "eviltrout" }); + var stream = user.get("stream"); assert.present(stream, "a user has a stream by default"); - assert.equal(stream.get('user'), user, "the stream points back to the user"); + assert.equal(stream.get("user"), user, "the stream points back to the user"); - assert.equal(stream.get('itemsLoaded'), 0, "no items are loaded by default"); - assert.blank(stream.get('content'), "no content by default"); - assert.blank(stream.get('filter'), "no filter by default"); + assert.equal(stream.get("itemsLoaded"), 0, "no items are loaded by default"); + assert.blank(stream.get("content"), "no content by default"); + assert.blank(stream.get("filter"), "no filter by default"); - assert.ok(!stream.get('loaded'), "the stream is not loaded by default"); + assert.ok(!stream.get("loaded"), "the stream is not loaded by default"); }); - -QUnit.test('filterParam', assert => { - var user = Discourse.User.create({id: 1, username: 'eviltrout'}); - var stream = user.get('stream'); +QUnit.test("filterParam", assert => { + var user = Discourse.User.create({ id: 1, username: "eviltrout" }); + var stream = user.get("stream"); // defaults to posts/topics - assert.equal(stream.get('filterParam'), "4,5"); + assert.equal(stream.get("filterParam"), "4,5"); - stream.set('filter', Discourse.UserAction.TYPES.likes_given); - assert.equal(stream.get('filterParam'), Discourse.UserAction.TYPES.likes_given); + stream.set("filter", Discourse.UserAction.TYPES.likes_given); + assert.equal( + stream.get("filterParam"), + Discourse.UserAction.TYPES.likes_given + ); - stream.set('filter', Discourse.UserAction.TYPES.replies); - assert.equal(stream.get('filterParam'), '6,9'); - -}); \ No newline at end of file + stream.set("filter", Discourse.UserAction.TYPES.replies); + assert.equal(stream.get("filterParam"), "6,9"); +}); diff --git a/test/javascripts/models/user-test.js.es6 b/test/javascripts/models/user-test.js.es6 index fb37937495..20ef656cf2 100644 --- a/test/javascripts/models/user-test.js.es6 +++ b/test/javascripts/models/user-test.js.es6 @@ -1,46 +1,68 @@ -import User from 'discourse/models/user'; -import Group from 'discourse/models/group'; +import User from "discourse/models/user"; +import Group from "discourse/models/group"; QUnit.module("model:user"); -QUnit.test('staff', assert =>{ - var user = User.create({id: 1, username: 'eviltrout'}); +QUnit.test("staff", assert => { + var user = User.create({ id: 1, username: "eviltrout" }); - assert.ok(!user.get('staff'), "user is not staff"); + assert.ok(!user.get("staff"), "user is not staff"); - user.toggleProperty('moderator'); - assert.ok(user.get('staff'), "moderators are staff"); + user.toggleProperty("moderator"); + assert.ok(user.get("staff"), "moderators are staff"); - user.setProperties({moderator: false, admin: true}); - assert.ok(user.get('staff'), "admins are staff"); + user.setProperties({ moderator: false, admin: true }); + assert.ok(user.get("staff"), "admins are staff"); }); -QUnit.test('searchContext', assert => { - var user = User.create({id: 1, username: 'EvilTrout'}); +QUnit.test("searchContext", assert => { + var user = User.create({ id: 1, username: "EvilTrout" }); - assert.deepEqual(user.get('searchContext'), {type: 'user', id: 'eviltrout', user: user}, "has a search context"); + assert.deepEqual( + user.get("searchContext"), + { type: "user", id: "eviltrout", user: user }, + "has a search context" + ); }); QUnit.test("isAllowedToUploadAFile", assert => { var user = User.create({ trust_level: 0, admin: true }); - assert.ok(user.isAllowedToUploadAFile("image"), "admin can always upload a file"); + assert.ok( + user.isAllowedToUploadAFile("image"), + "admin can always upload a file" + ); user.setProperties({ admin: false, moderator: true }); - assert.ok(user.isAllowedToUploadAFile("image"), "moderator can always upload a file"); + assert.ok( + user.isAllowedToUploadAFile("image"), + "moderator can always upload a file" + ); }); -QUnit.test('canMangeGroup', assert => { +QUnit.test("canMangeGroup", assert => { let user = User.create({ admin: true }); let group = Group.create({ automatic: true }); - assert.equal(user.canManageGroup(group), false, "automatic groups cannot be managed."); + assert.equal( + user.canManageGroup(group), + false, + "automatic groups cannot be managed." + ); group.set("automatic", false); - assert.equal(user.canManageGroup(group), true, "an admin should be able to manage the group"); + assert.equal( + user.canManageGroup(group), + true, + "an admin should be able to manage the group" + ); - user.set('admin', false); + user.set("admin", false); group.setProperties({ is_group_owner: true }); - assert.equal(user.canManageGroup(group), true, "a group owner should be able to manage the group"); -}); \ No newline at end of file + assert.equal( + user.canManageGroup(group), + true, + "a group owner should be able to manage the group" + ); +}); diff --git a/test/javascripts/widgets/actions-summary-test.js.es6 b/test/javascripts/widgets/actions-summary-test.js.es6 index f4eb8b7882..2a6fbbd344 100644 --- a/test/javascripts/widgets/actions-summary-test.js.es6 +++ b/test/javascripts/widgets/actions-summary-test.js.es6 @@ -1,80 +1,97 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('actions-summary'); +moduleForWidget("actions-summary"); -widgetTest('listing actions', { +widgetTest("listing actions", { template: '{{mount-widget widget="actions-summary" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { actionsSummary: [ - {id: 1, action: 'off_topic', description: 'very off topic'}, - {id: 2, action: 'spam', description: 'suspicious message'} + { id: 1, action: "off_topic", description: "very off topic" }, + { id: 2, action: "spam", description: "suspicious message" } ] }); }, test(assert) { - assert.equal(this.$('.post-actions .post-action').length, 2); + assert.equal(this.$(".post-actions .post-action").length, 2); - click('.post-action:eq(0) .action-link a'); + click(".post-action:eq(0) .action-link a"); andThen(() => { - assert.equal(this.$('.post-action:eq(0) img.avatar').length, 1, 'clicking it shows the user'); + assert.equal( + this.$(".post-action:eq(0) img.avatar").length, + 1, + "clicking it shows the user" + ); }); } }); -widgetTest('undo', { - template: '{{mount-widget widget="actions-summary" args=args undoPostAction=undoPostAction}}', +widgetTest("undo", { + template: + '{{mount-widget widget="actions-summary" args=args undoPostAction=undoPostAction}}', beforeEach() { - this.set('args', { + this.set("args", { actionsSummary: [ - {action: 'off_topic', description: 'very off topic', canUndo: true}, + { action: "off_topic", description: "very off topic", canUndo: true } ] }); - this.set('undoPostAction', () => this.undid = true); + this.set("undoPostAction", () => (this.undid = true)); }, test(assert) { - assert.equal(this.$('.post-actions .post-action').length, 1); + assert.equal(this.$(".post-actions .post-action").length, 1); - click('.action-link.undo'); + click(".action-link.undo"); andThen(() => { - assert.ok(this.undid, 'it triggered the action'); + assert.ok(this.undid, "it triggered the action"); }); } }); -widgetTest('deferFlags', { - template: '{{mount-widget widget="actions-summary" args=args deferPostActionFlags="deferPostActionFlags"}}', +widgetTest("deferFlags", { + template: + '{{mount-widget widget="actions-summary" args=args deferPostActionFlags="deferPostActionFlags"}}', beforeEach() { - this.set('args', { + this.set("args", { actionsSummary: [ - {action: 'off_topic', description: 'very off topic', canDeferFlags: true, count: 1}, + { + action: "off_topic", + description: "very off topic", + canDeferFlags: true, + count: 1 + } ] }); - this.on('deferPostActionFlags', () => this.deferred = true); + this.on("deferPostActionFlags", () => (this.deferred = true)); }, test(assert) { - assert.equal(this.$('.post-actions .post-action').length, 1); + assert.equal(this.$(".post-actions .post-action").length, 1); - click('.action-link.defer-flags'); + click(".action-link.defer-flags"); andThen(() => { - assert.ok(this.deferred, 'it triggered the action'); + assert.ok(this.deferred, "it triggered the action"); }); } }); -widgetTest('post deleted', { +widgetTest("post deleted", { template: '{{mount-widget widget="actions-summary" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { deleted_at: "2016-01-01", - deletedByUsername: 'eviltrout', - deletedByAvatarTemplate: '/images/avatar.png' + deletedByUsername: "eviltrout", + deletedByAvatarTemplate: "/images/avatar.png" }); }, test(assert) { - assert.ok(this.$('.post-action .d-icon-trash-o').length === 1, 'it has the deleted icon'); - assert.ok(this.$('.avatar[title=eviltrout]').length === 1, 'it has the deleted by avatar'); + assert.ok( + this.$(".post-action .d-icon-trash-o").length === 1, + "it has the deleted icon" + ); + assert.ok( + this.$(".avatar[title=eviltrout]").length === 1, + "it has the deleted by avatar" + ); } }); diff --git a/test/javascripts/widgets/button-test.js.es6 b/test/javascripts/widgets/button-test.js.es6 index 96bd4b245c..6286e3947b 100644 --- a/test/javascripts/widgets/button-test.js.es6 +++ b/test/javascripts/widgets/button-test.js.es6 @@ -1,43 +1,52 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('button'); +moduleForWidget("button"); -widgetTest('icon only button', { +widgetTest("icon only button", { template: '{{mount-widget widget="button" args=args}}', beforeEach() { - this.set('args', { icon: 'smile-o' }); + this.set("args", { icon: "smile-o" }); }, test(assert) { - assert.ok(this.$('button.btn.btn-icon.no-text').length, 'it has all the classes'); - assert.ok(this.$('button .d-icon.d-icon-smile-o').length, 'it has the icon'); + assert.ok( + this.$("button.btn.btn-icon.no-text").length, + "it has all the classes" + ); + assert.ok( + this.$("button .d-icon.d-icon-smile-o").length, + "it has the icon" + ); } }); -widgetTest('icon and text button', { +widgetTest("icon and text button", { template: '{{mount-widget widget="button" args=args}}', beforeEach() { - this.set('args', { icon: 'plus', label: 'topic.create' }); + this.set("args", { icon: "plus", label: "topic.create" }); }, test(assert) { - assert.ok(this.$('button.btn.btn-icon-text').length, 'it has all the classes'); - assert.ok(this.$('button .d-icon.d-icon-plus').length, 'it has the icon'); - assert.ok(this.$('button span.d-button-label').length, 'it has the label'); + assert.ok( + this.$("button.btn.btn-icon-text").length, + "it has all the classes" + ); + assert.ok(this.$("button .d-icon.d-icon-plus").length, "it has the icon"); + assert.ok(this.$("button span.d-button-label").length, "it has the label"); } }); -widgetTest('text only button', { +widgetTest("text only button", { template: '{{mount-widget widget="button" args=args}}', beforeEach() { - this.set('args', { label: 'topic.create' }); + this.set("args", { label: "topic.create" }); }, test(assert) { - assert.ok(this.$('button.btn.btn-text').length, 'it has all the classes'); - assert.ok(this.$('button span.d-button-label').length, 'it has the label'); + assert.ok(this.$("button.btn.btn-text").length, "it has all the classes"); + assert.ok(this.$("button span.d-button-label").length, "it has the label"); } }); diff --git a/test/javascripts/widgets/hamburger-menu-test.js.es6 b/test/javascripts/widgets/hamburger-menu-test.js.es6 index c8f67ccf1e..63759812bc 100644 --- a/test/javascripts/widgets/hamburger-menu-test.js.es6 +++ b/test/javascripts/widgets/hamburger-menu-test.js.es6 @@ -1,63 +1,66 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('hamburger-menu'); +moduleForWidget("hamburger-menu"); -widgetTest('prioritize faq', { +widgetTest("prioritize faq", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { - this.siteSettings.faq_url = 'http://example.com/faq'; - this.currentUser.set('read_faq', false); + this.siteSettings.faq_url = "http://example.com/faq"; + this.currentUser.set("read_faq", false); }, test(assert) { - assert.ok(this.$('.faq-priority').length); - assert.ok(!this.$('.faq-link').length); + assert.ok(this.$(".faq-priority").length); + assert.ok(!this.$(".faq-link").length); } }); -widgetTest('prioritize faq - user has read', { +widgetTest("prioritize faq - user has read", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { - this.siteSettings.faq_url = 'http://example.com/faq'; - this.currentUser.set('read_faq', true); + this.siteSettings.faq_url = "http://example.com/faq"; + this.currentUser.set("read_faq", true); }, test(assert) { - assert.ok(!this.$('.faq-priority').length); - assert.ok(this.$('.faq-link').length); + assert.ok(!this.$(".faq-priority").length); + assert.ok(this.$(".faq-link").length); } }); -widgetTest('staff menu - not staff', { +widgetTest("staff menu - not staff", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { - this.currentUser.set('staff', false); + this.currentUser.set("staff", false); }, test(assert) { - assert.ok(!this.$('.admin-link').length); + assert.ok(!this.$(".admin-link").length); } }); -widgetTest('staff menu', { +widgetTest("staff menu", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { - this.currentUser.setProperties({ staff: true, site_flagged_posts_count: 3 }); + this.currentUser.setProperties({ + staff: true, + site_flagged_posts_count: 3 + }); }, test(assert) { - assert.ok(this.$('.admin-link').length); - assert.ok(this.$('.flagged-posts-link').length); - assert.equal(this.$('.flagged-posts').text(), '3'); - assert.ok(!this.$('.settings-link').length); + assert.ok(this.$(".admin-link").length); + assert.ok(this.$(".flagged-posts-link").length); + assert.equal(this.$(".flagged-posts").text(), "3"); + assert.ok(!this.$(".settings-link").length); } }); -widgetTest('staff menu - admin', { +widgetTest("staff menu - admin", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { @@ -65,12 +68,11 @@ widgetTest('staff menu - admin', { }, test(assert) { - assert.ok(this.$('.settings-link').length); + assert.ok(this.$(".settings-link").length); } }); - -widgetTest('queued posts', { +widgetTest("queued posts", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { @@ -82,12 +84,12 @@ widgetTest('queued posts', { }, test(assert) { - assert.ok(this.$('.queued-posts-link').length); - assert.equal(this.$('.queued-posts').text(), '5'); + assert.ok(this.$(".queued-posts-link").length); + assert.equal(this.$(".queued-posts").text(), "5"); } }); -widgetTest('queued posts - disabled', { +widgetTest("queued posts - disabled", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { @@ -95,36 +97,35 @@ widgetTest('queued posts - disabled', { }, test(assert) { - assert.ok(!this.$('.queued-posts-link').length); + assert.ok(!this.$(".queued-posts-link").length); } }); - -widgetTest('logged in links', { +widgetTest("logged in links", { template: '{{mount-widget widget="hamburger-menu"}}', test(assert) { - assert.ok(this.$('.new-topics-link').length); - assert.ok(this.$('.unread-topics-link').length); + assert.ok(this.$(".new-topics-link").length); + assert.ok(this.$(".unread-topics-link").length); } }); -widgetTest('general links', { +widgetTest("general links", { template: '{{mount-widget widget="hamburger-menu"}}', anonymous: true, test(assert) { assert.ok(this.$("li[class='']").length === 0); - assert.ok(this.$('.latest-topics-link').length); - assert.ok(!this.$('.new-topics-link').length); - assert.ok(!this.$('.unread-topics-link').length); - assert.ok(this.$('.top-topics-link').length); - assert.ok(this.$('.badge-link').length); - assert.ok(this.$('.category-link').length > 0); + assert.ok(this.$(".latest-topics-link").length); + assert.ok(!this.$(".new-topics-link").length); + assert.ok(!this.$(".unread-topics-link").length); + assert.ok(this.$(".top-topics-link").length); + assert.ok(this.$(".badge-link").length); + assert.ok(this.$(".category-link").length > 0); } }); -widgetTest('badges link - disabled', { +widgetTest("badges link - disabled", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { @@ -132,27 +133,27 @@ widgetTest('badges link - disabled', { }, test(assert) { - assert.ok(!this.$('.badge-link').length); + assert.ok(!this.$(".badge-link").length); } }); -widgetTest('badges link', { +widgetTest("badges link", { template: '{{mount-widget widget="hamburger-menu"}}', test(assert) { - assert.ok(this.$('.badge-link').length); + assert.ok(this.$(".badge-link").length); } }); -widgetTest('user directory link', { +widgetTest("user directory link", { template: '{{mount-widget widget="hamburger-menu"}}', test(assert) { - assert.ok(this.$('.user-directory-link').length); + assert.ok(this.$(".user-directory-link").length); } }); -widgetTest('user directory link - disabled', { +widgetTest("user directory link - disabled", { template: '{{mount-widget widget="hamburger-menu"}}', beforeEach() { @@ -160,15 +161,15 @@ widgetTest('user directory link - disabled', { }, test(assert) { - assert.ok(!this.$('.user-directory-link').length); + assert.ok(!this.$(".user-directory-link").length); } }); -widgetTest('general links', { +widgetTest("general links", { template: '{{mount-widget widget="hamburger-menu"}}', test(assert) { - assert.ok(this.$('.about-link').length); - assert.ok(this.$('.keyboard-shortcuts-link').length); + assert.ok(this.$(".about-link").length); + assert.ok(this.$(".keyboard-shortcuts-link").length); } }); diff --git a/test/javascripts/widgets/header-test.js.es6 b/test/javascripts/widgets/header-test.js.es6 index f91ab7755e..7fdebfbb36 100644 --- a/test/javascripts/widgets/header-test.js.es6 +++ b/test/javascripts/widgets/header-test.js.es6 @@ -1,75 +1,78 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('header'); +moduleForWidget("header"); -widgetTest('rendering basics', { +widgetTest("rendering basics", { template: '{{mount-widget widget="header"}}', test(assert) { - assert.ok(this.$('header.d-header').length); - assert.ok(this.$('#site-logo').length); + assert.ok(this.$("header.d-header").length); + assert.ok(this.$("#site-logo").length); } }); -widgetTest('sign up / login buttons', { - template: '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}', +widgetTest("sign up / login buttons", { + template: + '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}', anonymous: true, beforeEach() { - this.set('args', { canSignUp: true }); - this.on('showCreateAccount', () => this.signupShown = true); - this.on('showLogin', () => this.loginShown = true); + this.set("args", { canSignUp: true }); + this.on("showCreateAccount", () => (this.signupShown = true)); + this.on("showLogin", () => (this.loginShown = true)); }, test(assert) { - assert.ok(this.$('button.sign-up-button').length); - assert.ok(this.$('button.login-button').length); + assert.ok(this.$("button.sign-up-button").length); + assert.ok(this.$("button.login-button").length); - click('button.sign-up-button'); + click("button.sign-up-button"); andThen(() => { assert.ok(this.signupShown); }); - click('button.login-button'); + click("button.login-button"); andThen(() => { assert.ok(this.loginShown); }); } }); -widgetTest('anon when login required', { - template: '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}', +widgetTest("anon when login required", { + template: + '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}', anonymous: true, beforeEach() { - this.set('args', { canSignUp: true }); - this.on('showCreateAccount', () => this.signupShown = true); - this.on('showLogin', () => this.loginShown = true); + this.set("args", { canSignUp: true }); + this.on("showCreateAccount", () => (this.signupShown = true)); + this.on("showLogin", () => (this.loginShown = true)); this.siteSettings.login_required = true; }, test(assert) { - assert.ok(exists('button.login-button')); - assert.ok(exists('button.sign-up-button')); - assert.ok(!exists('#search-button')); - assert.ok(!exists('#toggle-hamburger-menu')); + assert.ok(exists("button.login-button")); + assert.ok(exists("button.sign-up-button")); + assert.ok(!exists("#search-button")); + assert.ok(!exists("#toggle-hamburger-menu")); } }); -widgetTest('logged in when login required', { - template: '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}', +widgetTest("logged in when login required", { + template: + '{{mount-widget widget="header" showCreateAccount="showCreateAccount" showLogin="showLogin" args=args}}', beforeEach() { - this.set('args', { canSignUp: true }); - this.on('showCreateAccount', () => this.signupShown = true); - this.on('showLogin', () => this.loginShown = true); + this.set("args", { canSignUp: true }); + this.on("showCreateAccount", () => (this.signupShown = true)); + this.on("showLogin", () => (this.loginShown = true)); this.siteSettings.login_required = true; }, test(assert) { - assert.ok(!exists('button.login-button')); - assert.ok(!exists('button.sign-up-button')); - assert.ok(exists('#search-button')); - assert.ok(exists('#toggle-hamburger-menu')); - assert.ok(exists('#current-user')); + assert.ok(!exists("button.login-button")); + assert.ok(!exists("button.sign-up-button")); + assert.ok(exists("#search-button")); + assert.ok(exists("#toggle-hamburger-menu")); + assert.ok(exists("#current-user")); } }); diff --git a/test/javascripts/widgets/home-logo-test.js.es6 b/test/javascripts/widgets/home-logo-test.js.es6 index 70615b7805..e64b9ac41e 100644 --- a/test/javascripts/widgets/home-logo-test.js.es6 +++ b/test/javascripts/widgets/home-logo-test.js.es6 @@ -1,90 +1,90 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('home-logo'); +moduleForWidget("home-logo"); -const bigLogo = '/images/d-logo-sketch.png?test'; -const smallLogo = '/images/d-logo-sketch-small.png?test'; -const mobileLogo = '/images/d-logo-sketch.png?mobile'; +const bigLogo = "/images/d-logo-sketch.png?test"; +const smallLogo = "/images/d-logo-sketch-small.png?test"; +const mobileLogo = "/images/d-logo-sketch.png?mobile"; const title = "Cool Forum"; -widgetTest('basics', { +widgetTest("basics", { template: '{{mount-widget widget="home-logo" args=args}}', beforeEach() { this.siteSettings.logo_url = bigLogo; this.siteSettings.logo_small_url = smallLogo; this.siteSettings.title = title; - this.set('args', { minimized: false }); + this.set("args", { minimized: false }); }, test(assert) { - assert.ok(this.$('.title').length === 1); + assert.ok(this.$(".title").length === 1); - assert.ok(this.$('img#site-logo.logo-big').length === 1); - assert.equal(this.$('#site-logo').attr('src'), bigLogo); - assert.equal(this.$('#site-logo').attr('alt'), title); + assert.ok(this.$("img#site-logo.logo-big").length === 1); + assert.equal(this.$("#site-logo").attr("src"), bigLogo); + assert.equal(this.$("#site-logo").attr("alt"), title); } }); -widgetTest('basics - minimized', { +widgetTest("basics - minimized", { template: '{{mount-widget widget="home-logo" args=args}}', beforeEach() { this.siteSettings.logo_url = bigLogo; - this.siteSettings.logo_small_url= smallLogo; + this.siteSettings.logo_small_url = smallLogo; this.siteSettings.title = title; - this.set('args', { minimized: true }); + this.set("args", { minimized: true }); }, test(assert) { - assert.ok(this.$('img.logo-small').length === 1); - assert.equal(this.$('img.logo-small').attr('src'), smallLogo); - assert.equal(this.$('img.logo-small').attr('alt'), title); + assert.ok(this.$("img.logo-small").length === 1); + assert.equal(this.$("img.logo-small").attr("src"), smallLogo); + assert.equal(this.$("img.logo-small").attr("alt"), title); } }); -widgetTest('no logo', { +widgetTest("no logo", { template: '{{mount-widget widget="home-logo" args=args}}', beforeEach() { - this.siteSettings.logo_url = ''; - this.siteSettings.logo_small_url = ''; + this.siteSettings.logo_url = ""; + this.siteSettings.logo_small_url = ""; this.siteSettings.title = title; - this.set('args', { minimized: false }); + this.set("args", { minimized: false }); }, test(assert) { - assert.ok(this.$('h1#site-text-logo.text-logo').length === 1); - assert.equal(this.$('#site-text-logo').text(), title); + assert.ok(this.$("h1#site-text-logo.text-logo").length === 1); + assert.equal(this.$("#site-text-logo").text(), title); } }); -widgetTest('no logo - minimized', { +widgetTest("no logo - minimized", { template: '{{mount-widget widget="home-logo" args=args}}', beforeEach() { - this.siteSettings.logo_url = ''; - this.siteSettings.logo_small_url = ''; + this.siteSettings.logo_url = ""; + this.siteSettings.logo_small_url = ""; this.siteSettings.title = title; - this.set('args', { minimized: true }); + this.set("args", { minimized: true }); }, test(assert) { - assert.ok(this.$('.d-icon-home').length === 1); + assert.ok(this.$(".d-icon-home").length === 1); } }); -widgetTest('mobile logo', { +widgetTest("mobile logo", { template: '{{mount-widget widget="home-logo" args=args}}', beforeEach() { this.siteSettings.mobile_logo_url = mobileLogo; - this.siteSettings.logo_small_url= smallLogo; + this.siteSettings.logo_small_url = smallLogo; this.site.mobileView = true; }, test(assert) { - assert.ok(this.$('img#site-logo.logo-big').length === 1); - assert.equal(this.$('#site-logo').attr('src'), mobileLogo); + assert.ok(this.$("img#site-logo.logo-big").length === 1); + assert.equal(this.$("#site-logo").attr("src"), mobileLogo); } }); -widgetTest('mobile without logo', { +widgetTest("mobile without logo", { template: '{{mount-widget widget="home-logo" args=args}}', beforeEach() { this.siteSettings.logo_url = bigLogo; @@ -92,7 +92,7 @@ widgetTest('mobile without logo', { }, test(assert) { - assert.ok(this.$('img#site-logo.logo-big').length === 1); - assert.equal(this.$('#site-logo').attr('src'), bigLogo); + assert.ok(this.$("img#site-logo.logo-big").length === 1); + assert.equal(this.$("#site-logo").attr("src"), bigLogo); } }); diff --git a/test/javascripts/widgets/post-links-test.js.es6 b/test/javascripts/widgets/post-links-test.js.es6 index c3e8f0c267..ed40a6fadb 100644 --- a/test/javascripts/widgets/post-links-test.js.es6 +++ b/test/javascripts/widgets/post-links-test.js.es6 @@ -1,27 +1,39 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('post-links'); +moduleForWidget("post-links"); widgetTest("duplicate links", { template: '{{mount-widget widget="post-links" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { id: 2, links: [ - { title: "Evil Trout Link", url: "http://eviltrout.com", reflection: true }, - { title: "Evil Trout Link", url: "http://dupe.eviltrout.com", reflection: true } + { + title: "Evil Trout Link", + url: "http://eviltrout.com", + reflection: true + }, + { + title: "Evil Trout Link", + url: "http://dupe.eviltrout.com", + reflection: true + } ] }); }, test(assert) { - assert.equal(this.$('.post-links a.track-link').length, 1, 'it hides the dupe link'); + assert.equal( + this.$(".post-links a.track-link").length, + 1, + "it hides the dupe link" + ); } }); widgetTest("collapsed links", { template: '{{mount-widget widget="post-links" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { id: 1, links: [ { title: "Link 1", url: "http://eviltrout.com?1", reflection: true }, @@ -30,15 +42,15 @@ widgetTest("collapsed links", { { title: "Link 4", url: "http://eviltrout.com?4", reflection: true }, { title: "Link 5", url: "http://eviltrout.com?5", reflection: true }, { title: "Link 6", url: "http://eviltrout.com?6", reflection: true }, - { title: "Link 7", url: "http://eviltrout.com?7", reflection: true }, + { title: "Link 7", url: "http://eviltrout.com?7", reflection: true } ] }); }, test(assert) { - assert.ok(this.$('.expand-links').length === 1, 'collapsed by default'); - click('a.expand-links'); + assert.ok(this.$(".expand-links").length === 1, "collapsed by default"); + click("a.expand-links"); andThen(() => { - assert.equal(this.$('.post-links a.track-link').length, 7); + assert.equal(this.$(".post-links a.track-link").length, 7); }); } }); diff --git a/test/javascripts/widgets/post-stream-test.js.es6 b/test/javascripts/widgets/post-stream-test.js.es6 index 071d59db95..040084efc4 100644 --- a/test/javascripts/widgets/post-stream-test.js.es6 +++ b/test/javascripts/widgets/post-stream-test.js.es6 @@ -1,70 +1,144 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; -import Topic from 'discourse/models/topic'; -import Post from 'discourse/models/post'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; +import Topic from "discourse/models/topic"; +import Post from "discourse/models/post"; -moduleForWidget('post-stream'); +moduleForWidget("post-stream"); function postStreamTest(name, attrs) { widgetTest(name, { template: `{{mount-widget widget="post-stream" args=(hash posts=posts)}}`, beforeEach() { - const site = this.container.lookup('site:main'); + const site = this.container.lookup("site:main"); let posts = attrs.posts.call(this); - posts.forEach(p => p.set('site', site)); - this.set('posts', posts); + posts.forEach(p => p.set("site", site)); + this.set("posts", posts); }, test: attrs.test }); } -postStreamTest('basics', { +postStreamTest("basics", { posts() { - const site = this.container.lookup('site:main'); + const site = this.container.lookup("site:main"); const topic = Topic.create({ details: { created_by: { id: 123 } } }); return [ - Post.create({ topic, id: 1, post_number: 1, user_id: 123, primary_group_name: 'trout', avatar_template: '/images/avatar.png' }), - Post.create({ topic, id: 2, post_number: 2, post_type: site.get('post_types.moderator_action') }), + Post.create({ + topic, + id: 1, + post_number: 1, + user_id: 123, + primary_group_name: "trout", + avatar_template: "/images/avatar.png" + }), + Post.create({ + topic, + id: 2, + post_number: 2, + post_type: site.get("post_types.moderator_action") + }), Post.create({ topic, id: 3, post_number: 3, hidden: true }), - Post.create({ topic, id: 4, post_number: 4, post_type: site.get('post_types.whisper') }), - Post.create({ topic, id: 5, post_number: 5, wiki: true, via_email: true }), - Post.create({ topic, id: 6, post_number: 6, via_email: true, is_auto_generated: true }), + Post.create({ + topic, + id: 4, + post_number: 4, + post_type: site.get("post_types.whisper") + }), + Post.create({ + topic, + id: 5, + post_number: 5, + wiki: true, + via_email: true + }), + Post.create({ + topic, + id: 6, + post_number: 6, + via_email: true, + is_auto_generated: true + }) ]; }, test(assert) { - assert.equal(this.$('.post-stream').length, 1); - assert.equal(this.$('.topic-post').length, 6, 'renders all posts'); + assert.equal(this.$(".post-stream").length, 1); + assert.equal(this.$(".topic-post").length, 6, "renders all posts"); // look for special class bindings - assert.equal(this.$('.topic-post:eq(0).topic-owner').length, 1, 'it applies the topic owner class'); - assert.equal(this.$('.topic-post:eq(0).group-trout').length, 1, 'it applies the primary group class'); - assert.equal(this.$('.topic-post:eq(0).regular').length, 1, 'it applies the regular class'); - assert.equal(this.$('.topic-post:eq(1).moderator').length, 1, 'it applies the moderator class'); - assert.equal(this.$('.topic-post:eq(2).post-hidden').length, 1, 'it applies the hidden class'); - assert.equal(this.$('.topic-post:eq(3).whisper').length, 1, 'it applies the whisper class'); - assert.equal(this.$('.topic-post:eq(4).wiki').length, 1, 'it applies the wiki class'); + assert.equal( + this.$(".topic-post:eq(0).topic-owner").length, + 1, + "it applies the topic owner class" + ); + assert.equal( + this.$(".topic-post:eq(0).group-trout").length, + 1, + "it applies the primary group class" + ); + assert.equal( + this.$(".topic-post:eq(0).regular").length, + 1, + "it applies the regular class" + ); + assert.equal( + this.$(".topic-post:eq(1).moderator").length, + 1, + "it applies the moderator class" + ); + assert.equal( + this.$(".topic-post:eq(2).post-hidden").length, + 1, + "it applies the hidden class" + ); + assert.equal( + this.$(".topic-post:eq(3).whisper").length, + 1, + "it applies the whisper class" + ); + assert.equal( + this.$(".topic-post:eq(4).wiki").length, + 1, + "it applies the wiki class" + ); // it renders an article for the body with appropriate attributes - assert.equal(this.$('article#post_2').length, 1); - assert.equal(this.$('article[data-user-id=123]').length, 1); - assert.equal(this.$('article[data-post-id=3]').length, 1); - assert.equal(this.$('article#post_5.via-email').length, 1); - assert.equal(this.$('article#post_6.is-auto-generated').length, 1); + assert.equal(this.$("article#post_2").length, 1); + assert.equal(this.$("article[data-user-id=123]").length, 1); + assert.equal(this.$("article[data-post-id=3]").length, 1); + assert.equal(this.$("article#post_5.via-email").length, 1); + assert.equal(this.$("article#post_6.is-auto-generated").length, 1); - assert.equal(this.$('article:eq(0) .main-avatar').length, 1, 'renders the main avatar'); + assert.equal( + this.$("article:eq(0) .main-avatar").length, + 1, + "renders the main avatar" + ); } }); -postStreamTest('deleted posts', { +postStreamTest("deleted posts", { posts() { const topic = Topic.create({ details: { created_by: { id: 123 } } }); return [ - Post.create({ topic, id: 1, post_number: 1, deleted_at: new Date().toString() }), + Post.create({ + topic, + id: 1, + post_number: 1, + deleted_at: new Date().toString() + }) ]; }, test(assert) { - assert.equal(this.$('.topic-post.deleted').length, 1, 'it applies the deleted class'); - assert.equal(this.$('.deleted-user-avatar').length, 1, 'it has the trash avatar'); + assert.equal( + this.$(".topic-post.deleted").length, + 1, + "it applies the deleted class" + ); + assert.equal( + this.$(".deleted-user-avatar").length, + 1, + "it has the trash avatar" + ); } }); diff --git a/test/javascripts/widgets/post-test.js.es6 b/test/javascripts/widgets/post-test.js.es6 index 0296c9dcf3..4cc8fb34ce 100644 --- a/test/javascripts/widgets/post-test.js.es6 +++ b/test/javascripts/widgets/post-test.js.es6 @@ -1,146 +1,163 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('post'); +moduleForWidget("post"); -widgetTest('basic elements', { +widgetTest("basic elements", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { shareUrl: '/example', post_number: 1 }); + this.set("args", { shareUrl: "/example", post_number: 1 }); }, test(assert) { - assert.ok(this.$('.names').length, 'includes poster name'); + assert.ok(this.$(".names").length, "includes poster name"); - assert.ok(this.$('a.post-date').length, 'includes post date'); - assert.ok(this.$('a.post-date[data-share-url]').length); - assert.ok(this.$('a.post-date[data-post-number]').length); + assert.ok(this.$("a.post-date").length, "includes post date"); + assert.ok(this.$("a.post-date[data-share-url]").length); + assert.ok(this.$("a.post-date[data-post-number]").length); } }); -widgetTest('wiki', { - template: '{{mount-widget widget="post" args=args showHistory="showHistory"}}', +widgetTest("wiki", { + template: + '{{mount-widget widget="post" args=args showHistory="showHistory"}}', beforeEach() { - this.set('args', { wiki: true, version: 2, canViewEditHistory: true }); - this.on('showHistory', () => this.historyShown = true); + this.set("args", { wiki: true, version: 2, canViewEditHistory: true }); + this.on("showHistory", () => (this.historyShown = true)); }, test(assert) { - click('.post-info .wiki'); + click(".post-info .wiki"); andThen(() => { - assert.ok(this.historyShown, 'clicking the wiki icon displays the post history'); + assert.ok( + this.historyShown, + "clicking the wiki icon displays the post history" + ); }); } }); -widgetTest('wiki without revision', { +widgetTest("wiki without revision", { template: '{{mount-widget widget="post" args=args editPost="editPost"}}', beforeEach() { - this.set('args', { wiki: true, version: 1, canViewEditHistory: true }); - this.on('editPost', () => this.editPostCalled = true); + this.set("args", { wiki: true, version: 1, canViewEditHistory: true }); + this.on("editPost", () => (this.editPostCalled = true)); }, test(assert) { - click('.post-info .wiki'); + click(".post-info .wiki"); andThen(() => { - assert.ok(this.editPostCalled, 'clicking wiki icon edits the post'); + assert.ok(this.editPostCalled, "clicking wiki icon edits the post"); }); } }); -widgetTest('via-email', { - template: '{{mount-widget widget="post" args=args showRawEmail="showRawEmail"}}', +widgetTest("via-email", { + template: + '{{mount-widget widget="post" args=args showRawEmail="showRawEmail"}}', beforeEach() { - this.set('args', { via_email: true, canViewRawEmail: true }); - this.on('showRawEmail', () => this.rawEmailShown = true); + this.set("args", { via_email: true, canViewRawEmail: true }); + this.on("showRawEmail", () => (this.rawEmailShown = true)); }, test(assert) { - click('.post-info.via-email'); + click(".post-info.via-email"); andThen(() => { - assert.ok(this.rawEmailShown, 'clicking the enveloppe shows the raw email'); + assert.ok( + this.rawEmailShown, + "clicking the enveloppe shows the raw email" + ); }); } }); -widgetTest('via-email without permission', { - template: '{{mount-widget widget="post" args=args showRawEmail="showRawEmail"}}', +widgetTest("via-email without permission", { + template: + '{{mount-widget widget="post" args=args showRawEmail="showRawEmail"}}', beforeEach() { - this.set('args', { via_email: true, canViewRawEmail: false }); - this.on('showRawEmail', () => this.rawEmailShown = true); + this.set("args", { via_email: true, canViewRawEmail: false }); + this.on("showRawEmail", () => (this.rawEmailShown = true)); }, test(assert) { - click('.post-info.via-email'); + click(".post-info.via-email"); andThen(() => { - assert.ok(!this.rawEmailShown, `clicking the enveloppe doesn't show the raw email`); + assert.ok( + !this.rawEmailShown, + `clicking the enveloppe doesn't show the raw email` + ); }); } }); -widgetTest('history', { - template: '{{mount-widget widget="post" args=args showHistory="showHistory"}}', +widgetTest("history", { + template: + '{{mount-widget widget="post" args=args showHistory="showHistory"}}', beforeEach() { - this.set('args', { version: 3, canViewEditHistory: true }); - this.on('showHistory', () => this.historyShown = true); + this.set("args", { version: 3, canViewEditHistory: true }); + this.on("showHistory", () => (this.historyShown = true)); }, test(assert) { - click('.post-info.edits'); + click(".post-info.edits"); andThen(() => { - assert.ok(this.historyShown, 'clicking the pencil shows the history'); + assert.ok(this.historyShown, "clicking the pencil shows the history"); }); } }); -widgetTest('history without view permission', { - template: '{{mount-widget widget="post" args=args showHistory="showHistory"}}', +widgetTest("history without view permission", { + template: + '{{mount-widget widget="post" args=args showHistory="showHistory"}}', beforeEach() { - this.set('args', { version: 3, canViewEditHistory: false }); - this.on('showHistory', () => this.historyShown = true); + this.set("args", { version: 3, canViewEditHistory: false }); + this.on("showHistory", () => (this.historyShown = true)); }, test(assert) { - click('.post-info.edits'); + click(".post-info.edits"); andThen(() => { - assert.ok(!this.historyShown, `clicking the pencil doesn't show the history`); + assert.ok( + !this.historyShown, + `clicking the pencil doesn't show the history` + ); }); } }); -widgetTest('whisper', { +widgetTest("whisper", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { isWhisper: true }); + this.set("args", { isWhisper: true }); }, test(assert) { - assert.ok(this.$('.topic-post.whisper').length === 1); - assert.ok(this.$('.post-info.whisper').length === 1); + assert.ok(this.$(".topic-post.whisper").length === 1); + assert.ok(this.$(".post-info.whisper").length === 1); } }); -widgetTest('like count button', { +widgetTest("like count button", { template: '{{mount-widget widget="post" model=post args=args}}', beforeEach(store) { - const topic = store.createRecord('topic', {id: 123}); - const post = store.createRecord('post', { + const topic = store.createRecord("topic", { id: 123 }); + const post = store.createRecord("post", { id: 1, post_number: 1, topic, like_count: 3, - actions_summary: [ {id: 2, count: 1, hidden: false, can_act: true} ] + actions_summary: [{ id: 2, count: 1, hidden: false, can_act: true }] }); - this.set('post', post); - this.set('args', { likeCount: 1 }); + this.set("post", post); + this.set("args", { likeCount: 1 }); }, test(assert) { - assert.ok(this.$('button.like-count').length === 1); - assert.ok(this.$('.who-liked').length === 0); + assert.ok(this.$("button.like-count").length === 1); + assert.ok(this.$(".who-liked").length === 0); // toggle it on - click('button.like-count'); + click("button.like-count"); andThen(() => { - assert.ok(this.$('.who-liked').length === 1); - assert.ok(this.$('.who-liked a.trigger-user-card').length === 1); + assert.ok(this.$(".who-liked").length === 1); + assert.ok(this.$(".who-liked a.trigger-user-card").length === 1); }); // toggle it off - click('button.like-count'); + click("button.like-count"); andThen(() => { - assert.ok(this.$('.who-liked').length === 0); - assert.ok(this.$('.who-liked a.trigger-user-card').length === 0); + assert.ok(this.$(".who-liked").length === 0); + assert.ok(this.$(".who-liked a.trigger-user-card").length === 0); }); } }); @@ -148,83 +165,87 @@ widgetTest('like count button', { widgetTest(`like count with no likes`, { template: '{{mount-widget widget="post" model=post args=args}}', beforeEach() { - this.set('args', { likeCount: 0 }); + this.set("args", { likeCount: 0 }); }, test(assert) { - assert.ok(this.$('button.like-count').length === 0); + assert.ok(this.$("button.like-count").length === 0); } }); -widgetTest('share button', { +widgetTest("share button", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { shareUrl: 'http://share-me.example.com' }); + this.set("args", { shareUrl: "http://share-me.example.com" }); }, test(assert) { - assert.ok(!!this.$('.actions button[data-share-url]').length, 'it renders a share button'); + assert.ok( + !!this.$(".actions button[data-share-url]").length, + "it renders a share button" + ); } }); -widgetTest('liking', { - template: '{{mount-widget widget="post-menu" args=args toggleLike="toggleLike"}}', +widgetTest("liking", { + template: + '{{mount-widget widget="post-menu" args=args toggleLike="toggleLike"}}', beforeEach() { const args = { showLike: true, canToggleLike: true }; - this.set('args', args); - this.on('toggleLike', () => { + this.set("args", args); + this.on("toggleLike", () => { args.liked = !args.liked; args.likeCount = args.liked ? 1 : 0; }); }, test(assert) { - assert.ok(!!this.$('.actions button.like').length); - assert.ok(this.$('.actions button.like-count').length === 0); + assert.ok(!!this.$(".actions button.like").length); + assert.ok(this.$(".actions button.like-count").length === 0); - click('.actions button.like'); + click(".actions button.like"); andThen(() => { - assert.ok(!this.$('.actions button.like').length); - assert.ok(!!this.$('.actions button.has-like').length); - assert.ok(this.$('.actions button.like-count').length === 1); + assert.ok(!this.$(".actions button.like").length); + assert.ok(!!this.$(".actions button.has-like").length); + assert.ok(this.$(".actions button.like-count").length === 1); }); - click('.actions button.has-like'); + click(".actions button.has-like"); andThen(() => { - assert.ok(!!this.$('.actions button.like').length); - assert.ok(!this.$('.actions button.has-like').length); - assert.ok(this.$('.actions button.like-count').length === 0); + assert.ok(!!this.$(".actions button.like").length); + assert.ok(!this.$(".actions button.has-like").length); + assert.ok(this.$(".actions button.like-count").length === 0); }); } }); -widgetTest('anon liking', { - template: '{{mount-widget widget="post-menu" args=args showLogin="showLogin"}}', +widgetTest("anon liking", { + template: + '{{mount-widget widget="post-menu" args=args showLogin="showLogin"}}', anonymous: true, beforeEach() { const args = { showLike: true }; - this.set('args', args); - this.on("showLogin", () => this.loginShown = true); + this.set("args", args); + this.on("showLogin", () => (this.loginShown = true)); }, test(assert) { - assert.ok(!!this.$('.actions button.like').length); - assert.ok(this.$('.actions button.like-count').length === 0); + assert.ok(!!this.$(".actions button.like").length); + assert.ok(this.$(".actions button.like-count").length === 0); - click('.actions button.like'); + click(".actions button.like"); andThen(() => { assert.ok(this.loginShown); }); } }); - -widgetTest('edit button', { +widgetTest("edit button", { template: '{{mount-widget widget="post" args=args editPost="editPost"}}', beforeEach() { - this.set('args', { canEdit: true }); - this.on('editPost', () => this.editPostCalled = true); + this.set("args", { canEdit: true }); + this.on("editPost", () => (this.editPostCalled = true)); }, test(assert) { - click('button.edit'); + click("button.edit"); andThen(() => { - assert.ok(this.editPostCalled, 'it triggered the edit action'); + assert.ok(this.editPostCalled, "it triggered the edit action"); }); } }); @@ -232,37 +253,37 @@ widgetTest('edit button', { widgetTest(`edit button - can't edit`, { template: '{{mount-widget widget="post" args=args editPost="editPost"}}', beforeEach() { - this.set('args', { canEdit: false }); + this.set("args", { canEdit: false }); }, test(assert) { - assert.equal(this.$('button.edit').length, 0, `button is not displayed`); + assert.equal(this.$("button.edit").length, 0, `button is not displayed`); } }); -widgetTest('recover button', { +widgetTest("recover button", { template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', beforeEach() { - this.set('args', { canDelete: true }); - this.on('deletePost', () => this.deletePostCalled = true); + this.set("args", { canDelete: true }); + this.on("deletePost", () => (this.deletePostCalled = true)); }, test(assert) { - click('button.delete'); + click("button.delete"); andThen(() => { - assert.ok(this.deletePostCalled, 'it triggered the delete action'); + assert.ok(this.deletePostCalled, "it triggered the delete action"); }); } }); -widgetTest('delete topic button', { +widgetTest("delete topic button", { template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', beforeEach() { - this.set('args', { canDeleteTopic: true }); - this.on('deletePost', () => this.deletePostCalled = true); + this.set("args", { canDeleteTopic: true }); + this.on("deletePost", () => (this.deletePostCalled = true)); }, test(assert) { - click('button.delete'); + click("button.delete"); andThen(() => { - assert.ok(this.deletePostCalled, 'it triggered the delete action'); + assert.ok(this.deletePostCalled, "it triggered the delete action"); }); } }); @@ -270,21 +291,22 @@ widgetTest('delete topic button', { widgetTest(`delete topic button - can't delete`, { template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', beforeEach() { - this.set('args', { canDeleteTopic: false }); + this.set("args", { canDeleteTopic: false }); }, test(assert) { - assert.equal(this.$('button.delete').length, 0, `button is not displayed`); + assert.equal(this.$("button.delete").length, 0, `button is not displayed`); } }); -widgetTest('recover topic button', { - template: '{{mount-widget widget="post" args=args recoverPost="recoverPost"}}', +widgetTest("recover topic button", { + template: + '{{mount-widget widget="post" args=args recoverPost="recoverPost"}}', beforeEach() { - this.set('args', { canRecoverTopic: true }); - this.on('recoverPost', () => this.recovered = true); + this.set("args", { canRecoverTopic: true }); + this.on("recoverPost", () => (this.recovered = true)); }, test(assert) { - click('button.recover'); + click("button.recover"); andThen(() => assert.ok(this.recovered)); } }); @@ -292,23 +314,23 @@ widgetTest('recover topic button', { widgetTest(`recover topic button - can't recover`, { template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', beforeEach() { - this.set('args', { canRecoverTopic: false }); + this.set("args", { canRecoverTopic: false }); }, test(assert) { - assert.equal(this.$('button.recover').length, 0, `button is not displayed`); + assert.equal(this.$("button.recover").length, 0, `button is not displayed`); } }); -widgetTest('delete post button', { +widgetTest("delete post button", { template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', beforeEach() { - this.set('args', { canDelete: true }); - this.on('deletePost', () => this.deletePostCalled = true); + this.set("args", { canDelete: true }); + this.on("deletePost", () => (this.deletePostCalled = true)); }, test(assert) { - click('button.delete'); + click("button.delete"); andThen(() => { - assert.ok(this.deletePostCalled, 'it triggered the delete action'); + assert.ok(this.deletePostCalled, "it triggered the delete action"); }); } }); @@ -316,21 +338,22 @@ widgetTest('delete post button', { widgetTest(`delete post button - can't delete`, { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { canDelete: false }); + this.set("args", { canDelete: false }); }, test(assert) { - assert.equal(this.$('button.delete').length, 0, `button is not displayed`); + assert.equal(this.$("button.delete").length, 0, `button is not displayed`); } }); -widgetTest('recover post button', { - template: '{{mount-widget widget="post" args=args recoverPost="recoverPost"}}', +widgetTest("recover post button", { + template: + '{{mount-widget widget="post" args=args recoverPost="recoverPost"}}', beforeEach() { - this.set('args', { canRecover: true }); - this.on('recoverPost', () => this.recovered = true); + this.set("args", { canRecover: true }); + this.on("recoverPost", () => (this.recovered = true)); }, test(assert) { - click('button.recover'); + click("button.recover"); andThen(() => assert.ok(this.recovered)); } }); @@ -338,25 +361,25 @@ widgetTest('recover post button', { widgetTest(`recover post button - can't recover`, { template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', beforeEach() { - this.set('args', { canRecover: false }); + this.set("args", { canRecover: false }); }, test(assert) { - assert.equal(this.$('button.recover').length, 0, `button is not displayed`); + assert.equal(this.$("button.recover").length, 0, `button is not displayed`); } }); widgetTest(`flagging`, { template: '{{mount-widget widget="post" args=args showFlags="showFlags"}}', beforeEach() { - this.set('args', { canFlag: true }); - this.on('showFlags', () => this.flagsShown = true); + this.set("args", { canFlag: true }); + this.on("showFlags", () => (this.flagsShown = true)); }, test(assert) { - assert.ok(this.$('button.create-flag').length === 1); + assert.ok(this.$("button.create-flag").length === 1); - click('button.create-flag'); + click("button.create-flag"); andThen(() => { - assert.ok(this.flagsShown, 'it triggered the action'); + assert.ok(this.flagsShown, "it triggered the action"); }); } }); @@ -364,93 +387,98 @@ widgetTest(`flagging`, { widgetTest(`flagging: can't flag`, { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { canFlag: false }); + this.set("args", { canFlag: false }); }, test(assert) { - assert.ok(this.$('button.create-flag').length === 0); + assert.ok(this.$("button.create-flag").length === 0); } }); widgetTest(`read indicator`, { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { read: true }); + this.set("args", { read: true }); }, test(assert) { - assert.ok(this.$('.read-state.read').length); + assert.ok(this.$(".read-state.read").length); } }); widgetTest(`unread indicator`, { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { read: false }); + this.set("args", { read: false }); }, test(assert) { - assert.ok(this.$('.read-state').length); + assert.ok(this.$(".read-state").length); } }); widgetTest("reply directly above (supressed)", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { - replyToUsername: 'eviltrout', - replyToAvatarTemplate: '/images/avatar.png', + this.set("args", { + replyToUsername: "eviltrout", + replyToAvatarTemplate: "/images/avatar.png", replyDirectlyAbove: true }); }, test(assert) { - assert.equal(this.$('a.reply-to-tab').length, 0, 'hides the tab'); - assert.equal(this.$('.avoid-tab').length, 0, "doesn't have the avoid tab class"); + assert.equal(this.$("a.reply-to-tab").length, 0, "hides the tab"); + assert.equal( + this.$(".avoid-tab").length, + 0, + "doesn't have the avoid tab class" + ); } }); widgetTest("reply a few posts above (supressed)", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { - replyToUsername: 'eviltrout', - replyToAvatarTemplate: '/images/avatar.png', + this.set("args", { + replyToUsername: "eviltrout", + replyToAvatarTemplate: "/images/avatar.png", replyDirectlyAbove: false }); }, test(assert) { - assert.ok(this.$('a.reply-to-tab').length, 'shows the tab'); - assert.equal(this.$('.avoid-tab').length, 1, "has the avoid tab class"); + assert.ok(this.$("a.reply-to-tab").length, "shows the tab"); + assert.equal(this.$(".avoid-tab").length, 1, "has the avoid tab class"); } }); widgetTest("reply directly above", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { - replyToUsername: 'eviltrout', - replyToAvatarTemplate: '/images/avatar.png', + this.set("args", { + replyToUsername: "eviltrout", + replyToAvatarTemplate: "/images/avatar.png", replyDirectlyAbove: true }); this.siteSettings.suppress_reply_directly_above = false; }, test(assert) { - assert.equal(this.$('.avoid-tab').length, 1, "has the avoid tab class"); - click('a.reply-to-tab'); + assert.equal(this.$(".avoid-tab").length, 1, "has the avoid tab class"); + click("a.reply-to-tab"); andThen(() => { - assert.equal(this.$('section.embedded-posts.top .cooked').length, 1); - assert.equal(this.$('section.embedded-posts .d-icon-arrow-up').length, 1); + assert.equal(this.$("section.embedded-posts.top .cooked").length, 1); + assert.equal(this.$("section.embedded-posts .d-icon-arrow-up").length, 1); }); } }); widgetTest("cooked content hidden", { - template: '{{mount-widget widget="post" args=args expandHidden="expandHidden"}}', + template: + '{{mount-widget widget="post" args=args expandHidden="expandHidden"}}', beforeEach() { - this.set('args', { cooked_hidden: true }); - this.on('expandHidden', () => this.unhidden = true); + this.set("args", { cooked_hidden: true }); + this.on("expandHidden", () => (this.unhidden = true)); }, test(assert) { - click('.topic-body .expand-hidden'); + click(".topic-body .expand-hidden"); andThen(() => { - assert.ok(this.unhidden, 'triggers the action'); + assert.ok(this.unhidden, "triggers the action"); }); } }); @@ -458,13 +486,13 @@ widgetTest("cooked content hidden", { widgetTest("expand first post", { template: '{{mount-widget widget="post" model=post args=args}}', beforeEach(store) { - this.set('args', { expandablePost: true }); - this.set('post', store.createRecord('post', { id: 1234 })); + this.set("args", { expandablePost: true }); + this.set("post", store.createRecord("post", { id: 1234 })); }, test(assert) { - click('.topic-body .expand-post'); + click(".topic-body .expand-post"); andThen(() => { - assert.equal(this.$('.expand-post').length, 0, 'button is gone'); + assert.equal(this.$(".expand-post").length, 0, "button is gone"); }); } }); @@ -472,29 +500,30 @@ widgetTest("expand first post", { widgetTest("can't bookmark", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { canBookmark: false }); + this.set("args", { canBookmark: false }); }, test(assert) { - assert.equal(this.$('button.bookmark').length, 0); - assert.equal(this.$('button.bookmarked').length, 0); + assert.equal(this.$("button.bookmark").length, 0); + assert.equal(this.$("button.bookmarked").length, 0); } }); widgetTest("bookmark", { - template: '{{mount-widget widget="post" args=args toggleBookmark="toggleBookmark"}}', + template: + '{{mount-widget widget="post" args=args toggleBookmark="toggleBookmark"}}', beforeEach() { const args = { canBookmark: true }; - this.set('args', args); - this.on('toggleBookmark', () => args.bookmarked = true); + this.set("args", args); + this.on("toggleBookmark", () => (args.bookmarked = true)); }, test(assert) { - assert.equal(this.$('.post-menu-area .bookmark').length, 1); - assert.equal(this.$('button.bookmarked').length, 0); + assert.equal(this.$(".post-menu-area .bookmark").length, 1); + assert.equal(this.$("button.bookmarked").length, 0); - click('button.bookmark'); + click("button.bookmark"); andThen(() => { - assert.equal(this.$('button.bookmarked').length, 1); + assert.equal(this.$("button.bookmarked").length, 1); }); } }); @@ -502,58 +531,64 @@ widgetTest("bookmark", { widgetTest("can't show admin menu when you can't manage", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { canManage: false }); + this.set("args", { canManage: false }); }, test(assert) { - assert.equal(this.$('.post-menu-area .show-post-admin-menu').length, 0); + assert.equal(this.$(".post-menu-area .show-post-admin-menu").length, 0); } }); widgetTest("show admin menu", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { canManage: true }); + this.set("args", { canManage: true }); }, test(assert) { - assert.equal(this.$('.post-admin-menu').length, 0); - click('.post-menu-area .show-post-admin-menu'); + assert.equal(this.$(".post-admin-menu").length, 0); + click(".post-menu-area .show-post-admin-menu"); andThen(() => { - assert.equal(this.$('.post-admin-menu').length, 1, 'it shows the popup'); + assert.equal(this.$(".post-admin-menu").length, 1, "it shows the popup"); }); - click('.post-menu-area'); + click(".post-menu-area"); andThen(() => { - assert.equal(this.$('.post-admin-menu').length, 0, 'clicking outside clears the popup'); + assert.equal( + this.$(".post-admin-menu").length, + 0, + "clicking outside clears the popup" + ); }); } }); widgetTest("toggle moderator post", { - template: '{{mount-widget widget="post" args=args togglePostType="togglePostType"}}', + template: + '{{mount-widget widget="post" args=args togglePostType="togglePostType"}}', beforeEach() { - this.set('args', { canManage: true }); - this.on('togglePostType', () => this.toggled = true); + this.set("args", { canManage: true }); + this.on("togglePostType", () => (this.toggled = true)); }, test(assert) { - click('.post-menu-area .show-post-admin-menu'); - click('.post-admin-menu .toggle-post-type'); + click(".post-menu-area .show-post-admin-menu"); + click(".post-admin-menu .toggle-post-type"); andThen(() => { assert.ok(this.toggled); - assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + assert.equal(this.$(".post-admin-menu").length, 0, "also hides the menu"); }); } }); widgetTest("toggle moderator post", { - template: '{{mount-widget widget="post" args=args togglePostType="togglePostType"}}', + template: + '{{mount-widget widget="post" args=args togglePostType="togglePostType"}}', beforeEach() { - this.set('args', { canManage: true }); - this.on('togglePostType', () => this.toggled = true); + this.set("args", { canManage: true }); + this.on("togglePostType", () => (this.toggled = true)); }, test(assert) { - click('.post-menu-area .show-post-admin-menu'); - click('.post-admin-menu .toggle-post-type'); + click(".post-menu-area .show-post-admin-menu"); + click(".post-admin-menu .toggle-post-type"); andThen(() => { assert.ok(this.toggled); - assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + assert.equal(this.$(".post-admin-menu").length, 0, "also hides the menu"); }); } }); @@ -561,15 +596,15 @@ widgetTest("toggle moderator post", { widgetTest("rebake post", { template: '{{mount-widget widget="post" args=args rebakePost="rebakePost"}}', beforeEach() { - this.set('args', { canManage: true }); - this.on('rebakePost', () => this.baked = true); + this.set("args", { canManage: true }); + this.on("rebakePost", () => (this.baked = true)); }, test(assert) { - click('.post-menu-area .show-post-admin-menu'); - click('.post-admin-menu .rebuild-html'); + click(".post-menu-area .show-post-admin-menu"); + click(".post-admin-menu .rebuild-html"); andThen(() => { assert.ok(this.baked); - assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + assert.equal(this.$(".post-admin-menu").length, 0, "also hides the menu"); }); } }); @@ -577,44 +612,46 @@ widgetTest("rebake post", { widgetTest("unhide post", { template: '{{mount-widget widget="post" args=args unhidePost="unhidePost"}}', beforeEach() { - this.set('args', { canManage: true, hidden: true }); - this.on('unhidePost', () => this.unhidden = true); + this.set("args", { canManage: true, hidden: true }); + this.on("unhidePost", () => (this.unhidden = true)); }, test(assert) { - click('.post-menu-area .show-post-admin-menu'); - click('.post-admin-menu .unhide-post'); + click(".post-menu-area .show-post-admin-menu"); + click(".post-admin-menu .unhide-post"); andThen(() => { assert.ok(this.unhidden); - assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + assert.equal(this.$(".post-admin-menu").length, 0, "also hides the menu"); }); } }); widgetTest("change owner", { - template: '{{mount-widget widget="post" args=args changePostOwner="changePostOwner"}}', + template: + '{{mount-widget widget="post" args=args changePostOwner="changePostOwner"}}', beforeEach() { this.currentUser.admin = true; - this.set('args', { canManage: true }); - this.on('changePostOwner', () => this.owned = true); + this.set("args", { canManage: true }); + this.on("changePostOwner", () => (this.owned = true)); }, test(assert) { - click('.post-menu-area .show-post-admin-menu'); - click('.post-admin-menu .change-owner'); + click(".post-menu-area .show-post-admin-menu"); + click(".post-admin-menu .change-owner"); andThen(() => { assert.ok(this.owned); - assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + assert.equal(this.$(".post-admin-menu").length, 0, "also hides the menu"); }); } }); widgetTest("reply", { - template: '{{mount-widget widget="post" args=args replyToPost="replyToPost"}}', + template: + '{{mount-widget widget="post" args=args replyToPost="replyToPost"}}', beforeEach() { - this.set('args', { canCreatePost: true }); - this.on('replyToPost', () => this.replied = true); + this.set("args", { canCreatePost: true }); + this.on("replyToPost", () => (this.replied = true)); }, test(assert) { - click('.post-controls .create'); + click(".post-controls .create"); andThen(() => { assert.ok(this.replied); }); @@ -624,20 +661,20 @@ widgetTest("reply", { widgetTest("reply - without permissions", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { canCreatePost: false }); + this.set("args", { canCreatePost: false }); }, test(assert) { - assert.equal(this.$('.post-controls .create').length, 0); + assert.equal(this.$(".post-controls .create").length, 0); } }); widgetTest("replies - no replies", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', {replyCount: 0}); + this.set("args", { replyCount: 0 }); }, test(assert) { - assert.equal(this.$('button.show-replies').length, 0); + assert.equal(this.$("button.show-replies").length, 0); } }); @@ -645,10 +682,10 @@ widgetTest("replies - multiple replies", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { this.siteSettings.suppress_reply_directly_below = true; - this.set('args', {replyCount: 2, replyDirectlyBelow: true}); + this.set("args", { replyCount: 2, replyDirectlyBelow: true }); }, test(assert) { - assert.equal(this.$('button.show-replies').length, 1); + assert.equal(this.$("button.show-replies").length, 1); } }); @@ -656,10 +693,10 @@ widgetTest("replies - one below, suppressed", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { this.siteSettings.suppress_reply_directly_below = true; - this.set('args', {replyCount: 1, replyDirectlyBelow: true}); + this.set("args", { replyCount: 1, replyDirectlyBelow: true }); }, test(assert) { - assert.equal(this.$('button.show-replies').length, 0); + assert.equal(this.$("button.show-replies").length, 0); } }); @@ -667,13 +704,16 @@ widgetTest("replies - one below, not suppressed", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { this.siteSettings.suppress_reply_directly_below = false; - this.set('args', {id: 6654, replyCount: 1, replyDirectlyBelow: true}); + this.set("args", { id: 6654, replyCount: 1, replyDirectlyBelow: true }); }, test(assert) { - click('button.show-replies'); + click("button.show-replies"); andThen(() => { - assert.equal(this.$('section.embedded-posts.bottom .cooked').length, 1); - assert.equal(this.$('section.embedded-posts .d-icon-arrow-down').length, 1); + assert.equal(this.$("section.embedded-posts.bottom .cooked").length, 1); + assert.equal( + this.$("section.embedded-posts .d-icon-arrow-down").length, + 1 + ); }); } }); @@ -681,31 +721,36 @@ widgetTest("replies - one below, not suppressed", { widgetTest("topic map not shown", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { showTopicMap: false }); + this.set("args", { showTopicMap: false }); }, test(assert) { - assert.equal(this.$('.topic-map').length, 0); + assert.equal(this.$(".topic-map").length, 0); } }); widgetTest("topic map - few posts", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { showTopicMap: true, topicPostsCount: 2, - participants: [ - {username: 'eviltrout'}, - {username: 'codinghorror'}, - ] + participants: [{ username: "eviltrout" }, { username: "codinghorror" }] }); }, test(assert) { - assert.equal(this.$('li.avatars a.poster').length, 0, 'shows no participants when collapsed'); + assert.equal( + this.$("li.avatars a.poster").length, + 0, + "shows no participants when collapsed" + ); - click('nav.buttons button'); + click("nav.buttons button"); andThen(() => { - assert.equal(this.$('.topic-map-expanded a.poster').length, 2, 'shows all when expanded'); + assert.equal( + this.$(".topic-map-expanded a.poster").length, + 2, + "shows all when expanded" + ); }); } }); @@ -713,26 +758,34 @@ widgetTest("topic map - few posts", { widgetTest("topic map - participants", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { showTopicMap: true, topicPostsCount: 10, participants: [ - {username: 'eviltrout'}, - {username: 'codinghorror'}, - {username: 'sam'}, - {username: 'ZogStrIP'}, + { username: "eviltrout" }, + { username: "codinghorror" }, + { username: "sam" }, + { username: "ZogStrIP" } ], - userFilters: ['sam', 'codinghorror'] + userFilters: ["sam", "codinghorror"] }); }, test(assert) { - assert.equal(this.$('li.avatars a.poster').length, 3, 'limits to three participants'); + assert.equal( + this.$("li.avatars a.poster").length, + 3, + "limits to three participants" + ); - click('nav.buttons button'); + click("nav.buttons button"); andThen(() => { - assert.equal(this.$('li.avatars a.poster').length, 0); - assert.equal(this.$('.topic-map-expanded a.poster').length, 4, 'shows all when expanded'); - assert.equal(this.$('a.poster.toggled').length, 2, 'two are toggled'); + assert.equal(this.$("li.avatars a.poster").length, 0); + assert.equal( + this.$(".topic-map-expanded a.poster").length, + 4, + "shows all when expanded" + ); + assert.equal(this.$("a.poster.toggled").length, 2, "two are toggled"); }); } }); @@ -740,34 +793,42 @@ widgetTest("topic map - participants", { widgetTest("topic map - links", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { showTopicMap: true, topicLinks: [ - {url: 'http://link1.example.com', clicks: 0}, - {url: 'http://link2.example.com', clicks: 0}, - {url: 'http://link3.example.com', clicks: 0}, - {url: 'http://link4.example.com', clicks: 0}, - {url: 'http://link5.example.com', clicks: 0}, - {url: 'http://link6.example.com', clicks: 0}, + { url: "http://link1.example.com", clicks: 0 }, + { url: "http://link2.example.com", clicks: 0 }, + { url: "http://link3.example.com", clicks: 0 }, + { url: "http://link4.example.com", clicks: 0 }, + { url: "http://link5.example.com", clicks: 0 }, + { url: "http://link6.example.com", clicks: 0 } ] }); }, test(assert) { - assert.equal(this.$('.topic-map').length, 1); - assert.equal(this.$('.map.map-collapsed').length, 1); - assert.equal(this.$('.topic-map-expanded').length, 0); + assert.equal(this.$(".topic-map").length, 1); + assert.equal(this.$(".map.map-collapsed").length, 1); + assert.equal(this.$(".topic-map-expanded").length, 0); - click('nav.buttons button'); + click("nav.buttons button"); andThen(() => { - assert.equal(this.$('.map.map-collapsed').length, 0); - assert.equal(this.$('.topic-map .d-icon-chevron-up').length, 1); - assert.equal(this.$('.topic-map-expanded').length, 1); - assert.equal(this.$('.topic-map-expanded .topic-link').length, 5, 'it limits the links displayed'); + assert.equal(this.$(".map.map-collapsed").length, 0); + assert.equal(this.$(".topic-map .d-icon-chevron-up").length, 1); + assert.equal(this.$(".topic-map-expanded").length, 1); + assert.equal( + this.$(".topic-map-expanded .topic-link").length, + 5, + "it limits the links displayed" + ); }); - click('.link-summary button'); + click(".link-summary button"); andThen(() => { - assert.equal(this.$('.topic-map-expanded .topic-link').length, 6, 'all links now shown'); + assert.equal( + this.$(".topic-map-expanded .topic-link").length, + 6, + "all links now shown" + ); }); } }); @@ -775,23 +836,24 @@ widgetTest("topic map - links", { widgetTest("topic map - no summary", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { showTopicMap: true }); + this.set("args", { showTopicMap: true }); }, test(assert) { - assert.equal(this.$('.toggle-summary').length, 0); + assert.equal(this.$(".toggle-summary").length, 0); } }); widgetTest("topic map - has summary", { - template: '{{mount-widget widget="post" args=args toggleSummary="toggleSummary"}}', + template: + '{{mount-widget widget="post" args=args toggleSummary="toggleSummary"}}', beforeEach() { - this.set('args', { showTopicMap: true, hasTopicSummary: true }); - this.on('toggleSummary', () => this.summaryToggled = true); + this.set("args", { showTopicMap: true, hasTopicSummary: true }); + this.on("toggleSummary", () => (this.summaryToggled = true)); }, test(assert) { - assert.equal(this.$('.toggle-summary').length, 1); + assert.equal(this.$(".toggle-summary").length, 1); - click('.toggle-summary button'); + click(".toggle-summary button"); andThen(() => assert.ok(this.summaryToggled)); } }); @@ -799,15 +861,15 @@ widgetTest("topic map - has summary", { widgetTest("pm map", { template: '{{mount-widget widget="post" args=args}}', beforeEach() { - this.set('args', { + this.set("args", { showTopicMap: true, showPMMap: true, allowedGroups: [], - allowedUsers: [ Ember.Object.create({ username: 'eviltrout' }) ] + allowedUsers: [Ember.Object.create({ username: "eviltrout" })] }); }, test(assert) { - assert.equal(this.$('.private-message-map').length, 1); - assert.equal(this.$('.private-message-map .user').length, 1); + assert.equal(this.$(".private-message-map").length, 1); + assert.equal(this.$(".private-message-map .user").length, 1); } }); diff --git a/test/javascripts/widgets/poster-name-test.js.es6 b/test/javascripts/widgets/poster-name-test.js.es6 index c8ab8391d7..c618f6f32b 100644 --- a/test/javascripts/widgets/poster-name-test.js.es6 +++ b/test/javascripts/widgets/poster-name-test.js.es6 @@ -1,57 +1,58 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('poster-name'); +moduleForWidget("poster-name"); -widgetTest('basic rendering', { +widgetTest("basic rendering", { template: '{{mount-widget widget="poster-name" args=args}}', beforeEach() { - this.set('args', { - username: 'eviltrout', - usernameUrl: '/u/eviltrout', - name: 'Robin Ward', - user_title: 'Trout Master' }); + this.set("args", { + username: "eviltrout", + usernameUrl: "/u/eviltrout", + name: "Robin Ward", + user_title: "Trout Master" + }); }, test(assert) { - assert.ok(this.$('.names').length); - assert.ok(this.$('span.username').length); - assert.ok(this.$('a[data-user-card=eviltrout]').length); - assert.equal(this.$('.username a').text(), 'eviltrout'); - assert.equal(this.$('.full-name a').text(), 'Robin Ward'); - assert.equal(this.$('.user-title').text(), 'Trout Master'); + assert.ok(this.$(".names").length); + assert.ok(this.$("span.username").length); + assert.ok(this.$("a[data-user-card=eviltrout]").length); + assert.equal(this.$(".username a").text(), "eviltrout"); + assert.equal(this.$(".full-name a").text(), "Robin Ward"); + assert.equal(this.$(".user-title").text(), "Trout Master"); } }); -widgetTest('extra classes and glyphs', { +widgetTest("extra classes and glyphs", { template: '{{mount-widget widget="poster-name" args=args}}', beforeEach() { - this.set('args', { - username: 'eviltrout', - usernameUrl: '/u/eviltrout', + this.set("args", { + username: "eviltrout", + usernameUrl: "/u/eviltrout", staff: true, admin: true, moderator: true, new_user: true, - primary_group_name: 'fish' - }); + primary_group_name: "fish" + }); }, test(assert) { - assert.ok(this.$('span.staff').length); - assert.ok(this.$('span.admin').length); - assert.ok(this.$('span.moderator').length); - assert.ok(this.$('.d-icon-shield').length); - assert.ok(this.$('span.new-user').length); - assert.ok(this.$('span.fish').length); + assert.ok(this.$("span.staff").length); + assert.ok(this.$("span.admin").length); + assert.ok(this.$("span.moderator").length); + assert.ok(this.$(".d-icon-shield").length); + assert.ok(this.$("span.new-user").length); + assert.ok(this.$("span.fish").length); } }); -widgetTest('disable display name on posts', { +widgetTest("disable display name on posts", { template: '{{mount-widget widget="poster-name" args=args}}', beforeEach() { this.siteSettings.display_name_on_posts = false; - this.set('args', { username: 'eviltrout', name: 'Robin Ward' }); + this.set("args", { username: "eviltrout", name: "Robin Ward" }); }, test(assert) { - assert.equal(this.$('.full-name').length, 0); + assert.equal(this.$(".full-name").length, 0); } }); @@ -60,9 +61,9 @@ widgetTest("doesn't render a name if it's similar to the username", { beforeEach() { this.siteSettings.prioritize_username_in_ux = true; this.siteSettings.display_name_on_posts = true; - this.set('args', { username: 'eviltrout', name: 'evil-trout' }); + this.set("args", { username: "eviltrout", name: "evil-trout" }); }, test(assert) { - assert.equal(this.$('.second').length, 0); + assert.equal(this.$(".second").length, 0); } }); diff --git a/test/javascripts/widgets/topic-participant-test.js.es6 b/test/javascripts/widgets/topic-participant-test.js.es6 index 67ba90a063..88f309da1e 100644 --- a/test/javascripts/widgets/topic-participant-test.js.es6 +++ b/test/javascripts/widgets/topic-participant-test.js.es6 @@ -1,43 +1,49 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('topic-participant'); +moduleForWidget("topic-participant"); -widgetTest('one post', { +widgetTest("one post", { template: '{{mount-widget widget="topic-participant" args=args}}', beforeEach() { - this.set('args', { - username: 'test', - avatar_template: '/images/avatar.png', + this.set("args", { + username: "test", + avatar_template: "/images/avatar.png", post_count: 1 }); }, test(assert) { - assert.ok(exists('a.poster.trigger-user-card')); - assert.ok(!exists('span.post-count'), "don't show count for only 1 post"); - assert.ok(!exists('.avatar-flair'), "no avatar flair"); + assert.ok(exists("a.poster.trigger-user-card")); + assert.ok(!exists("span.post-count"), "don't show count for only 1 post"); + assert.ok(!exists(".avatar-flair"), "no avatar flair"); } }); -widgetTest('many posts, a primary group with flair', { +widgetTest("many posts, a primary group with flair", { template: '{{mount-widget widget="topic-participant" args=args}}', beforeEach() { - this.set('args', { - username: 'test', - avatar_template: '/images/avatar.png', + this.set("args", { + username: "test", + avatar_template: "/images/avatar.png", post_count: 5, - primary_group_name: 'devs', + primary_group_name: "devs", primary_group_flair_url: "/images/d-logo-sketch-small.png", primary_group_flair_bg_color: "222" }); }, test(assert) { - assert.ok(exists('a.poster.trigger-user-card')); - assert.ok(exists('span.post-count'), "show count for many posts"); - assert.ok(exists('.group-devs a.poster'), "add class for the group outside the link"); - assert.ok(exists('.avatar-flair.avatar-flair-devs'), "show flair with group class"); + assert.ok(exists("a.poster.trigger-user-card")); + assert.ok(exists("span.post-count"), "show count for many posts"); + assert.ok( + exists(".group-devs a.poster"), + "add class for the group outside the link" + ); + assert.ok( + exists(".avatar-flair.avatar-flair-devs"), + "show flair with group class" + ); } }); diff --git a/test/javascripts/widgets/user-menu-test.js.es6 b/test/javascripts/widgets/user-menu-test.js.es6 index 19b4640a65..951e045185 100644 --- a/test/javascripts/widgets/user-menu-test.js.es6 +++ b/test/javascripts/widgets/user-menu-test.js.es6 @@ -1,80 +1,81 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; -moduleForWidget('user-menu'); +moduleForWidget("user-menu"); -widgetTest('basics', { +widgetTest("basics", { template: '{{mount-widget widget="user-menu"}}', test(assert) { - assert.ok(this.$('.user-menu').length); - assert.ok(this.$('.user-activity-link').length); - assert.ok(this.$('.user-bookmarks-link').length); - assert.ok(this.$('.user-preferences-link').length); - assert.ok(this.$('.notifications').length); - assert.ok(this.$('.dismiss-link').length); + assert.ok(this.$(".user-menu").length); + assert.ok(this.$(".user-activity-link").length); + assert.ok(this.$(".user-bookmarks-link").length); + assert.ok(this.$(".user-preferences-link").length); + assert.ok(this.$(".notifications").length); + assert.ok(this.$(".dismiss-link").length); } }); -widgetTest('log out', { +widgetTest("log out", { template: '{{mount-widget widget="user-menu" logout="logout"}}', beforeEach() { - this.on('logout', () => this.loggedOut = true); + this.on("logout", () => (this.loggedOut = true)); }, test(assert) { - assert.ok(this.$('.logout').length); + assert.ok(this.$(".logout").length); - click('.logout'); + click(".logout"); andThen(() => { assert.ok(this.loggedOut); }); } }); -widgetTest('private messages - disabled', { +widgetTest("private messages - disabled", { template: '{{mount-widget widget="user-menu"}}', beforeEach() { this.siteSettings.enable_personal_messages = false; }, test(assert) { - assert.ok(!this.$('.user-pms-link').length); + assert.ok(!this.$(".user-pms-link").length); } }); -widgetTest('private messages - enabled', { +widgetTest("private messages - enabled", { template: '{{mount-widget widget="user-menu"}}', beforeEach() { this.siteSettings.enable_personal_messages = true; }, test(assert) { - assert.ok(this.$('.user-pms-link').length); + assert.ok(this.$(".user-pms-link").length); } }); -widgetTest('anonymous', { - template: '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}', +widgetTest("anonymous", { + template: + '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}', beforeEach() { this.currentUser.setProperties({ is_anonymous: false, trust_level: 3 }); this.siteSettings.allow_anonymous_posting = true; this.siteSettings.anonymous_posting_min_trust_level = 3; - this.on('toggleAnonymous', () => this.anonymous = true); + this.on("toggleAnonymous", () => (this.anonymous = true)); }, test(assert) { - assert.ok(this.$('.enable-anonymous').length); - click('.enable-anonymous'); + assert.ok(this.$(".enable-anonymous").length); + click(".enable-anonymous"); andThen(() => { assert.ok(this.anonymous); }); } }); -widgetTest('anonymous - disabled', { +widgetTest("anonymous - disabled", { template: '{{mount-widget widget="user-menu"}}', beforeEach() { @@ -82,23 +83,24 @@ widgetTest('anonymous - disabled', { }, test(assert) { - assert.ok(!this.$('.enable-anonymous').length); + assert.ok(!this.$(".enable-anonymous").length); } }); -widgetTest('anonymous - switch back', { - template: '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}', +widgetTest("anonymous - switch back", { + template: + '{{mount-widget widget="user-menu" toggleAnonymous="toggleAnonymous"}}', beforeEach() { this.currentUser.setProperties({ is_anonymous: true }); this.siteSettings.allow_anonymous_posting = true; - this.on('toggleAnonymous', () => this.anonymous = true); + this.on("toggleAnonymous", () => (this.anonymous = true)); }, test(assert) { - assert.ok(this.$('.disable-anonymous').length); - click('.disable-anonymous'); + assert.ok(this.$(".disable-anonymous").length); + click(".disable-anonymous"); andThen(() => { assert.ok(this.anonymous); }); diff --git a/test/javascripts/widgets/widget-test.js.es6 b/test/javascripts/widgets/widget-test.js.es6 index 78502f6ccd..b8afd17cc5 100644 --- a/test/javascripts/widgets/widget-test.js.es6 +++ b/test/javascripts/widgets/widget-test.js.es6 @@ -1,24 +1,24 @@ -import { moduleForWidget, widgetTest } from 'helpers/widget-test'; -import { createWidget } from 'discourse/widgets/widget'; -import { withPluginApi } from 'discourse/lib/plugin-api'; -import hbs from 'discourse/widgets/hbs-compiler'; +import { moduleForWidget, widgetTest } from "helpers/widget-test"; +import { createWidget } from "discourse/widgets/widget"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import hbs from "discourse/widgets/hbs-compiler"; -moduleForWidget('base'); +moduleForWidget("base"); -widgetTest('widget attributes are passed in via args', { +widgetTest("widget attributes are passed in via args", { template: `{{mount-widget widget="hello-test" args=args}}`, beforeEach() { - createWidget('hello-test', { - tagName: 'div.test', + createWidget("hello-test", { + tagName: "div.test", template: hbs`Hello {{attrs.name}}` }); - this.set('args', { name: 'Robin' }); + this.set("args", { name: "Robin" }); }, test(assert) { - assert.equal(this.$('.test').text(), "Hello Robin"); + assert.equal(this.$(".test").text(), "Hello Robin"); } }); @@ -26,15 +26,15 @@ widgetTest("hbs template - no tagName", { template: `{{mount-widget widget="hbs-test" args=args}}`, beforeEach() { - createWidget('hbs-test', { + createWidget("hbs-test", { template: hbs`
      Hello {{attrs.name}}
      ` }); - this.set('args', { name: 'Robin' }); + this.set("args", { name: "Robin" }); }, test(assert) { - assert.equal(this.$('div.test').text(), "Hello Robin"); + assert.equal(this.$("div.test").text(), "Hello Robin"); } }); @@ -42,84 +42,87 @@ widgetTest("hbs template - with tagName", { template: `{{mount-widget widget="hbs-test" args=args}}`, beforeEach() { - createWidget('hbs-test', { - tagName: 'div.test', + createWidget("hbs-test", { + tagName: "div.test", template: hbs`Hello {{attrs.name}}` }); - this.set('args', { name: 'Robin' }); + this.set("args", { name: "Robin" }); }, test(assert) { - assert.equal(this.$('div.test').text(), "Hello Robin"); + assert.equal(this.$("div.test").text(), "Hello Robin"); } }); -widgetTest('buildClasses', { +widgetTest("buildClasses", { template: `{{mount-widget widget="classname-test" args=args}}`, beforeEach() { - createWidget('classname-test', { - tagName: 'div.test', + createWidget("classname-test", { + tagName: "div.test", buildClasses(attrs) { - return ['static', attrs.dynamic]; + return ["static", attrs.dynamic]; } }); - this.set('args', { dynamic: 'cool-class' }); + this.set("args", { dynamic: "cool-class" }); }, test(assert) { - assert.ok(this.$('.test.static.cool-class').length, 'it has all the classes'); + assert.ok( + this.$(".test.static.cool-class").length, + "it has all the classes" + ); } }); -widgetTest('buildAttributes', { +widgetTest("buildAttributes", { template: `{{mount-widget widget="attributes-test" args=args}}`, beforeEach() { - createWidget('attributes-test', { - tagName: 'div.test', + createWidget("attributes-test", { + tagName: "div.test", buildAttributes(attrs) { - return { "data-evil": 'trout', "aria-label": attrs.label }; + return { "data-evil": "trout", "aria-label": attrs.label }; } }); - this.set('args', { label: 'accessibility' }); + this.set("args", { label: "accessibility" }); }, test(assert) { - assert.ok(this.$('.test[data-evil=trout]').length); - assert.ok(this.$('.test[aria-label=accessibility]').length); + assert.ok(this.$(".test[data-evil=trout]").length); + assert.ok(this.$(".test[aria-label=accessibility]").length); } }); -widgetTest('buildId', { +widgetTest("buildId", { template: `{{mount-widget widget="id-test" args=args}}`, beforeEach() { - createWidget('id-test', { + createWidget("id-test", { buildId(attrs) { return `test-${attrs.id}`; } }); - this.set('args', { id: 1234 }); + this.set("args", { id: 1234 }); }, test(assert) { - assert.ok(this.$('#test-1234').length); + assert.ok(this.$("#test-1234").length); } }); -widgetTest('widget state', { +widgetTest("widget state", { template: `{{mount-widget widget="state-test"}}`, beforeEach() { - createWidget('state-test', { - tagName: 'button.test', + createWidget("state-test", { + tagName: "button.test", buildKey: () => `button-test`, template: hbs`{{state.clicks}} clicks`, @@ -134,23 +137,23 @@ widgetTest('widget state', { }, test(assert) { - assert.ok(this.$('button.test').length, 'it renders the button'); - assert.equal(this.$('button.test').text(), "0 clicks"); + assert.ok(this.$("button.test").length, "it renders the button"); + assert.equal(this.$("button.test").text(), "0 clicks"); - click(this.$('button')); + click(this.$("button")); andThen(() => { - assert.equal(this.$('button.test').text(), "1 clicks"); + assert.equal(this.$("button.test").text(), "1 clicks"); }); } }); -widgetTest('widget update with promise', { +widgetTest("widget update with promise", { template: `{{mount-widget widget="promise-test"}}`, beforeEach() { - createWidget('promise-test', { - tagName: 'button.test', - buildKey: () => 'promise-test', + createWidget("promise-test", { + tagName: "button.test", + buildKey: () => "promise-test", template: hbs` {{#if state.name}} {{state.name}} @@ -171,30 +174,40 @@ widgetTest('widget update with promise', { }, test(assert) { - assert.equal(this.$('button.test').text().trim(), "No name"); + assert.equal( + this.$("button.test") + .text() + .trim(), + "No name" + ); - click(this.$('button')); + click(this.$("button")); andThen(() => { - assert.equal(this.$('button.test').text().trim(), "Robin"); + assert.equal( + this.$("button.test") + .text() + .trim(), + "Robin" + ); }); } }); -widgetTest('widget attaching', { +widgetTest("widget attaching", { template: `{{mount-widget widget="attach-test"}}`, beforeEach() { - createWidget('test-embedded', { tagName: 'div.embedded' }); + createWidget("test-embedded", { tagName: "div.embedded" }); - createWidget('attach-test', { - tagName: 'div.container', + createWidget("attach-test", { + tagName: "div.container", template: hbs`{{attach widget="test-embedded" attrs=attrs}}` }); }, test(assert) { - assert.ok(this.$('.container').length, "renders container"); - assert.ok(this.$('.container .embedded').length, "renders attached"); + assert.ok(this.$(".container").length, "renders container"); + assert.ok(this.$(".container .embedded").length, "renders attached"); } }); @@ -202,13 +215,13 @@ widgetTest("handlebars d-icon", { template: `{{mount-widget widget="hbs-icon-test" args=args}}`, beforeEach() { - createWidget('hbs-icon-test', { + createWidget("hbs-icon-test", { template: hbs`{{d-icon "arrow-down"}}` }); }, test(assert) { - assert.equal(this.$('i.fa.fa-arrow-down').length, 1); + assert.equal(this.$("i.fa.fa-arrow-down").length, 1); } }); @@ -216,34 +229,36 @@ widgetTest("handlebars i18n", { template: `{{mount-widget widget="hbs-i18n-test" args=args}}`, beforeEach() { - createWidget('hbs-i18n-test', { + createWidget("hbs-i18n-test", { template: hbs` {{i18n "hbs_test0"}} {{i18n attrs.key}} test ` }); - I18n.extras = [ { - "hbs_test0": "evil", - "hbs_test1": "trout" - } ]; - this.set('args', { key: 'hbs_test1' }); + I18n.extras = [ + { + hbs_test0: "evil", + hbs_test1: "trout" + } + ]; + this.set("args", { key: "hbs_test1" }); }, test(assert) { // comin up - assert.equal(this.$('span.string').text(), 'evil'); - assert.equal(this.$('span.var').text(), 'trout'); - assert.equal(this.$('a').prop('title'), 'evil'); + assert.equal(this.$("span.string").text(), "evil"); + assert.equal(this.$("span.var").text(), "trout"); + assert.equal(this.$("a").prop("title"), "evil"); } }); -widgetTest('handlebars #each', { +widgetTest("handlebars #each", { template: `{{mount-widget widget="hbs-each-test" args=args}}`, beforeEach() { - createWidget('hbs-each-test', { - tagName: 'ul', + createWidget("hbs-each-test", { + tagName: "ul", template: hbs` {{#each attrs.items as |item|}}
    • {{item}}
    • @@ -251,77 +266,76 @@ widgetTest('handlebars #each', { ` }); - this.set('args', { - items: ['one', 'two', 'three'] + this.set("args", { + items: ["one", "two", "three"] }); }, test(assert) { - assert.equal(this.$('ul li').length, 3); - assert.equal(this.$('ul li:eq(0)').text(), "one"); + assert.equal(this.$("ul li").length, 3); + assert.equal(this.$("ul li:eq(0)").text(), "one"); } - }); -widgetTest('widget decorating', { +widgetTest("widget decorating", { template: `{{mount-widget widget="decorate-test"}}`, beforeEach() { - createWidget('decorate-test', { - tagName: 'div.decorate', + createWidget("decorate-test", { + tagName: "div.decorate", template: hbs`main content` }); - withPluginApi('0.1', api => { - api.decorateWidget('decorate-test:before', dec => { - return dec.h('b', 'before'); + withPluginApi("0.1", api => { + api.decorateWidget("decorate-test:before", dec => { + return dec.h("b", "before"); }); - api.decorateWidget('decorate-test:after', dec => { - return dec.h('i', 'after'); + api.decorateWidget("decorate-test:after", dec => { + return dec.h("i", "after"); }); }); }, test(assert) { - assert.ok(this.$('.decorate').length); - assert.equal(this.$('.decorate b').text(), 'before'); - assert.equal(this.$('.decorate i').text(), 'after'); + assert.ok(this.$(".decorate").length); + assert.equal(this.$(".decorate b").text(), "before"); + assert.equal(this.$(".decorate i").text(), "after"); } }); -widgetTest('widget settings', { +widgetTest("widget settings", { template: `{{mount-widget widget="settings-test"}}`, beforeEach() { - createWidget('settings-test', { - tagName: 'div.settings', + createWidget("settings-test", { + tagName: "div.settings", template: hbs`age is {{settings.age}}`, settings: { age: 36 } }); }, test(assert) { - assert.equal(this.$('.settings').text(), 'age is 36'); + assert.equal(this.$(".settings").text(), "age is 36"); } }); -widgetTest('override settings', { +widgetTest("override settings", { template: `{{mount-widget widget="ov-settings-test"}}`, beforeEach() { - createWidget('ov-settings-test', { - tagName: 'div.settings', + createWidget("ov-settings-test", { + tagName: "div.settings", template: hbs`age is {{settings.age}}`, - settings: { age: 36 }, + settings: { age: 36 } }); - withPluginApi('0.1', api => { - api.changeWidgetSetting('ov-settings-test', 'age', 37); + withPluginApi("0.1", api => { + api.changeWidgetSetting("ov-settings-test", "age", 37); }); }, test(assert) { - assert.equal(this.$('.settings').text(), 'age is 37'); + assert.equal(this.$(".settings").text(), "age is 37"); } }); diff --git a/test/smoke_test.js b/test/smoke_test.js index 095d2b95ab..3bc3e7559b 100644 --- a/test/smoke_test.js +++ b/test/smoke_test.js @@ -16,7 +16,7 @@ const path = require('path'); const browser = await puppeteer.launch({ // when debugging localy setting headless to "false" can be very helpful headless: true, - args: ["--disable-local-storage"] + args: ["--disable-local-storage", "--no-sandbox"] }); const page = await browser.newPage(); @@ -59,11 +59,11 @@ const path = require('path'); return exec(description, fn, assertion); }; - page.on('console', msg => console.log(`PAGE LOG: ${msg.text}`)); + page.on('console', msg => console.log(`PAGE LOG: ${msg.text()}`)); page.on('response', resp => { - if (resp.status !== 200) { - console.log("FAILED HTTP REQUEST TO " + resp.url + " Status is: " + resp.status); + if (resp.status() !== 200) { + console.log("FAILED HTTP REQUEST TO " + resp.url() + " Status is: " + resp.status()); } return resp; }); @@ -218,20 +218,10 @@ const path = require('path'); return page.type("#reply-control .d-editor-input", post); }); - await assert("waiting for the preview", () => { - let promise = page.waitForSelector(".d-editor-preview p", + await exec("waiting for the preview", () => { + return page.waitForXPath("//div[contains(@class, 'd-editor-preview') and contains(.//p, 'I can even write a reply')]", { visible: true } ); - - promise = promise.then(() => { - return page.evaluate(() => { - return document.querySelector(".d-editor-preview").innerText; - }); - }); - - return promise; - }, output => { - return output.match("I can even write a reply"); }); await exec("submit the topic", () => { diff --git a/yarn.lock b/yarn.lock index e73d58eb46..d908e1a096 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,54 +2,77 @@ # yarn lockfile v1 -"@babel/code-frame@7.0.0-beta.31": - version "7.0.0-beta.31" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz#473d021ecc573a2cce1c07d5b509d5215f46ba35" +"@babel/code-frame@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.44.tgz#2a02643368de80916162be70865c97774f3adbd9" + dependencies: + "@babel/highlight" "7.0.0-beta.44" + +"@babel/generator@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.44.tgz#c7e67b9b5284afcf69b309b50d7d37f3e5033d42" + dependencies: + "@babel/types" "7.0.0-beta.44" + jsesc "^2.5.1" + lodash "^4.2.0" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/helper-function-name@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.44.tgz#e18552aaae2231100a6e485e03854bc3532d44dd" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.44" + "@babel/template" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-get-function-arity@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.44.tgz#d03ca6dd2b9f7b0b1e6b32c56c72836140db3a15" + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/helper-split-export-declaration@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.44.tgz#c0b351735e0fbcb3822c8ad8db4e583b05ebd9dc" + dependencies: + "@babel/types" "7.0.0-beta.44" + +"@babel/highlight@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.44.tgz#18c94ce543916a80553edcdcf681890b200747d5" dependencies: chalk "^2.0.0" esutils "^2.0.2" js-tokens "^3.0.0" -"@babel/helper-function-name@7.0.0-beta.31": - version "7.0.0-beta.31" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.31.tgz#afe63ad799209989348b1109b44feb66aa245f57" +"@babel/template@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f" dependencies: - "@babel/helper-get-function-arity" "7.0.0-beta.31" - "@babel/template" "7.0.0-beta.31" - "@babel/traverse" "7.0.0-beta.31" - "@babel/types" "7.0.0-beta.31" - -"@babel/helper-get-function-arity@7.0.0-beta.31": - version "7.0.0-beta.31" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.31.tgz#1176d79252741218e0aec872ada07efb2b37a493" - dependencies: - "@babel/types" "7.0.0-beta.31" - -"@babel/template@7.0.0-beta.31": - version "7.0.0-beta.31" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.31.tgz#577bb29389f6c497c3e7d014617e7d6713f68bda" - dependencies: - "@babel/code-frame" "7.0.0-beta.31" - "@babel/types" "7.0.0-beta.31" - babylon "7.0.0-beta.31" + "@babel/code-frame" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" lodash "^4.2.0" -"@babel/traverse@7.0.0-beta.31": - version "7.0.0-beta.31" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.31.tgz#db399499ad74aefda014f0c10321ab255134b1df" +"@babel/traverse@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.44.tgz#a970a2c45477ad18017e2e465a0606feee0d2966" dependencies: - "@babel/code-frame" "7.0.0-beta.31" - "@babel/helper-function-name" "7.0.0-beta.31" - "@babel/types" "7.0.0-beta.31" - babylon "7.0.0-beta.31" - debug "^3.0.1" - globals "^10.0.0" + "@babel/code-frame" "7.0.0-beta.44" + "@babel/generator" "7.0.0-beta.44" + "@babel/helper-function-name" "7.0.0-beta.44" + "@babel/helper-split-export-declaration" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + debug "^3.1.0" + globals "^11.1.0" invariant "^2.2.0" lodash "^4.2.0" -"@babel/types@7.0.0-beta.31": - version "7.0.0-beta.31" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.31.tgz#42c9c86784f674c173fb21882ca9643334029de4" +"@babel/types@7.0.0-beta.44": + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.44.tgz#6b1b164591f77dec0a0342aca995f2d046b3a757" dependencies: esutils "^2.0.2" lodash "^4.2.0" @@ -63,9 +86,9 @@ version "0.3.29" resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.3.29.tgz#7f2ad7ec55f914482fc9b1ec4bb1ae6028d46066" -"@types/node@6.0.66": - version "6.0.66" - resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.66.tgz#5680b74a6135d33d4c00447e7c3dc691a4601625" +"@types/node@^9.3.0": + version "9.6.20" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.20.tgz#b59a1bd357ae2df7d44d5ac98e9b64eb96ea1fef" "@types/rimraf@^0.0.28": version "0.0.28" @@ -81,9 +104,9 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.2.1.tgz#317ac7821826c22c702d66189ab8359675f135d7" +acorn@^5.5.0: + version "5.6.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7" agent-base@^4.1.0: version "4.1.2" @@ -158,18 +181,20 @@ babel-code-frame@^6.22.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-eslint@^8.0.3: - version "8.0.3" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.3.tgz#f29ecf02336be438195325cd47c468da81ee4e98" +babel-eslint@^8.2.3: + version "8.2.3" + resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.2.3.tgz#1a2e6681cc9bc4473c32899e59915e19cd6733cf" dependencies: - "@babel/code-frame" "7.0.0-beta.31" - "@babel/traverse" "7.0.0-beta.31" - "@babel/types" "7.0.0-beta.31" - babylon "7.0.0-beta.31" + "@babel/code-frame" "7.0.0-beta.44" + "@babel/traverse" "7.0.0-beta.44" + "@babel/types" "7.0.0-beta.44" + babylon "7.0.0-beta.44" + eslint-scope "~3.7.1" + eslint-visitor-keys "^1.0.0" -babylon@7.0.0-beta.31: - version "7.0.0-beta.31" - resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.31.tgz#7ec10f81e0e456fd0f855ad60fa30c2ac454283f" +babylon@7.0.0-beta.44: + version "7.0.0-beta.44" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" balanced-match@^1.0.0: version "1.0.0" @@ -214,22 +239,22 @@ chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" -chrome-launcher@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.10.0.tgz#4291ab1d20bb097da80b085e8c8c8e6b3e3d72c9" +chrome-launcher@^0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.10.2.tgz#f7d860ddec627b6f01015736b5ae1e33b3d165b1" dependencies: "@types/core-js" "^0.9.41" "@types/mkdirp" "^0.3.29" - "@types/node" "6.0.66" + "@types/node" "^9.3.0" "@types/rimraf" "^0.0.28" is-wsl "^1.1.0" lighthouse-logger "^1.0.0" mkdirp "0.5.1" rimraf "^2.6.1" -chrome-remote-interface@^0.25.4: - version "0.25.4" - resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.25.4.tgz#3a84aa9ef053dc2fd25d3b4c30d7501cb5f73883" +chrome-remote-interface@^0.25.6: + version "0.25.6" + resolved "https://registry.yarnpkg.com/chrome-remote-interface/-/chrome-remote-interface-0.25.6.tgz#172bc82d87091a2ae90711a7b6c940da64ce32ef" dependencies: commander "2.11.x" ws "3.3.x" @@ -296,7 +321,7 @@ debug@2.6.9, debug@^2.6.8: dependencies: ms "2.0.0" -debug@^3.0.1, debug@^3.1.0: +debug@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: @@ -318,9 +343,9 @@ del@^2.0.2: pinkie-promise "^2.0.0" rimraf "^2.2.8" -doctrine@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.0.2.tgz#68f96ce8efc56cc42651f1faadb4f175273b0075" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" dependencies: esutils "^2.0.2" @@ -338,28 +363,32 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" -eslint-scope@^3.7.1: +eslint-scope@^3.7.1, eslint-scope@~3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8" dependencies: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint@^4.13.1: - version "4.13.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.13.1.tgz#0055e0014464c7eb7878caf549ef2941992b444f" +eslint-visitor-keys@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" + +eslint@^4.19.1: + version "4.19.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.19.1.tgz#32d1d653e1d90408854bfb296f076ec7e186a300" dependencies: ajv "^5.3.0" babel-code-frame "^6.22.0" chalk "^2.1.0" concat-stream "^1.6.0" cross-spawn "^5.1.0" - debug "^3.0.1" - doctrine "^2.0.2" + debug "^3.1.0" + doctrine "^2.1.0" eslint-scope "^3.7.1" - espree "^3.5.2" + eslint-visitor-keys "^1.0.0" + espree "^3.5.4" esquery "^1.0.0" - estraverse "^4.2.0" esutils "^2.0.2" file-entry-cache "^2.0.0" functional-red-black-tree "^1.0.1" @@ -380,18 +409,19 @@ eslint@^4.13.1: path-is-inside "^1.0.2" pluralize "^7.0.0" progress "^2.0.0" + regexpp "^1.0.1" require-uncached "^1.0.3" semver "^5.3.0" strip-ansi "^4.0.0" strip-json-comments "~2.0.1" - table "^4.0.1" + table "4.0.2" text-table "~0.2.0" -espree@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.2.tgz#756ada8b979e9dcfcdb30aad8d1a9304a905e1ca" +espree@^3.5.4: + version "3.5.4" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.4.tgz#b0f447187c8a8bed944b815a660bddf5deb5d1a7" dependencies: - acorn "^5.2.1" + acorn "^5.5.0" acorn-jsx "^3.0.0" esprima@^4.0.0: @@ -411,7 +441,7 @@ esrecurse@^4.1.0: estraverse "^4.1.0" object-assign "^4.0.1" -estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" @@ -495,14 +525,14 @@ glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" -globals@^10.0.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-10.4.0.tgz#5c477388b128a9e4c5c5d01c7a2aca68c68b2da7" - globals@^11.0.1: version "11.1.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.1.0.tgz#632644457f5f0e3ae711807183700ebf2e4633e4" +globals@^11.1.0: + version "11.5.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.5.0.tgz#6bc840de6771173b191f13d3a9c94d441ee92642" + globby@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" @@ -634,6 +664,10 @@ js-yaml@^3.9.1: argparse "^1.0.7" esprima "^4.0.0" +jsesc@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + json-schema-traverse@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" @@ -672,9 +706,9 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" -mime@^1.3.4: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" +mime@^2.0.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369" mimic-fn@^1.0.0: version "1.1.0" @@ -779,6 +813,10 @@ prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" +prettier@1.13.4: + version "1.13.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.4.tgz#31bbae6990f13b1093187c731766a14036fa72e6" + process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -795,14 +833,14 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" -puppeteer@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-0.13.0.tgz#2e6956205f2c640964c2107f620ae1eef8bde8fd" +puppeteer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.4.0.tgz#437f0f3450d76e437185c0bf06f446e80f184692" dependencies: - debug "^2.6.8" + debug "^3.1.0" extract-zip "^1.6.5" https-proxy-agent "^2.1.0" - mime "^1.3.4" + mime "^2.0.3" progress "^2.0.0" proxy-from-env "^1.0.0" rimraf "^2.6.1" @@ -820,6 +858,10 @@ readable-stream@^2.2.2: string_decoder "~1.0.3" util-deprecate "~1.0.1" +regexpp@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-1.1.0.tgz#0e3516dd0b7904f413d2d4193dce4618c3a689ab" + require-uncached@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" @@ -888,6 +930,10 @@ slice-ansi@1.0.0: dependencies: is-fullwidth-code-point "^2.0.0" +source-map@^0.5.0: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -931,7 +977,7 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" -table@^4.0.1: +table@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" dependencies: @@ -960,6 +1006,10 @@ to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"