From 5f988c74f2c0929e34abc86527d95250881501ec Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 4 Jun 2015 00:10:35 -0700 Subject: [PATCH 01/60] fix minor alignment issues with expanded posts --- app/assets/stylesheets/desktop/topic-post.scss | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 012c03285e..09ee591874 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -234,12 +234,18 @@ nav.post-controls { // WARNING: overflow hide is required for quoted / embedded images // which expect "normal" post width, but expansions are narrower overflow: hidden; - padding: 10px 15px 15px 15px; + padding: 15px 15px 0 15px; + } + + // this is covered by .topic-body .regular on a normal post + // but no such class structure exists for an embedded, expanded post + .cooked { + margin-top: 15px; } .topic-avatar { padding-left: 15px; - padding-top: 0; + padding-top: 15px; } // bottom means "reply expansion" below a post @@ -266,7 +272,7 @@ nav.post-controls { padding-right: 0; } - .post-date { color: scale-color($primary, $lightness: 50%); } + .post-date { color: scale-color($primary, $lightness: 60%); } .fa-arrow-up, .fa-arrow-down { margin-left: 5px; } .reply:first-of-type .row { border-top: none; } @@ -282,7 +288,7 @@ nav.post-controls { color: scale-color($primary, $lightness: 30%); } } - .arrow {color: scale-color($primary, $lightness: 50%);} + .arrow {color: scale-color($primary, $lightness: 60%);} } .post-action { From 9675b426ee99ca39b43cc9f526e28b2c5129bc14 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 4 Jun 2015 00:39:19 -0700 Subject: [PATCH 02/60] lighter quote controls --- app/assets/stylesheets/common/base/topic-post.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 6f46267ba8..18f869b73a 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -68,7 +68,7 @@ aside.quote { } .quote-controls, .quote-controls .back:before, .quote-controls .quote-other-topic:before { - color: scale-color($primary, $lightness: 60%); + color: scale-color($primary, $lightness: 70%); } .cooked .highlight { From fb7baeb7a2d3d44c81fb8dc8bc311e1af1f45bae Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 4 Jun 2015 11:35:36 -0400 Subject: [PATCH 03/60] Never enqueue posts from staff --- lib/new_post_manager.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb index 1cbc38e191..7c17135d02 100644 --- a/lib/new_post_manager.rb +++ b/lib/new_post_manager.rb @@ -29,6 +29,8 @@ class NewPostManager end def self.user_needs_approval?(user) + return false if user.staff? + (user.post_count < SiteSetting.approve_post_count) || (user.trust_level < SiteSetting.approve_unless_trust_level.to_i) end From 9b489506d09781373992b6b35aa8cefcb9e7538b Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 5 Jun 2015 01:03:11 +1000 Subject: [PATCH 04/60] update memory profiler, oj and lru redux --- Gemfile.lock | 6 +++--- config/initializers/06-mini_profiler.rb | 7 +++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2b2f3aa8d6..ff5e238b0f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,11 +148,11 @@ GEM libv8 (3.16.14.7) listen (0.7.3) logster (0.8.1) - lru_redux (0.8.4) + lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) treetop (~> 1.4.8) - memory_profiler (0.9.0) + memory_profiler (0.9.3) message_bus (1.0.12) rack (>= 1.1.3) redis @@ -182,7 +182,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (~> 1.2) - oj (2.12.0) + oj (2.12.9) omniauth (1.2.2) hashie (>= 1.2, < 4) rack (~> 1.0) diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb index bd13624a60..5f7e017cb5 100644 --- a/config/initializers/06-mini_profiler.rb +++ b/config/initializers/06-mini_profiler.rb @@ -3,8 +3,11 @@ if Rails.configuration.respond_to?(:load_mini_profiler) && Rails.configuration.l require 'rack-mini-profiler' require 'flamegraph' - # TODO support Ruby 2.2 once bundler fixes itself - require 'memory_profiler' if RUBY_VERSION >= "2.1.0" && RUBY_VERSION < "2.2.0" + begin + require 'memory_profiler' if RUBY_VERSION >= "2.1.0" + rescue => e + STDERR.put "#{e} failed to require mini profiler" + end # initialization is skipped so trigger it Rack::MiniProfilerRails.initialize!(Rails.application) From 803083fc2ea005a97963d438773d48fafae34467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 4 Jun 2015 19:16:52 +0200 Subject: [PATCH 05/60] we don't care about convert output/errors --- app/models/optimized_image.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 029ac1b033..d29560ab12 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -159,7 +159,7 @@ class OptimizedImage < ActiveRecord::Base end def self.convert_with(instructions, to) - `convert #{instructions.join(" ")}` + `convert #{instructions.join(" ")} &> /dev/null` return false if $?.exitstatus != 0 ImageOptim.new.optimize_image!(to) From f1637fc11e0acd4fb205e2fba5917010edb38c81 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 4 Jun 2015 15:56:17 -0400 Subject: [PATCH 06/60] FEATURE: plugins can register a custom admin quick start topic that will be seeded into new sites --- db/fixtures/999_topics.rb | 13 ++++++++-- lib/discourse_plugin_registry.rb | 9 +++++++ lib/plugin/instance.rb | 14 ++++++++++- .../discourse_plugin_registry_spec.rb | 24 +++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) diff --git a/db/fixtures/999_topics.rb b/db/fixtures/999_topics.rb index 1ab472c300..0f813877b3 100644 --- a/db/fixtures/999_topics.rb +++ b/db/fixtures/999_topics.rb @@ -54,6 +54,15 @@ if seed_welcome_topics post.topic.update_pinned(true) end - welcome = File.read(Rails.root + 'docs/ADMIN-QUICK-START-GUIDE.md') - PostCreator.create(Discourse.system_user, raw: welcome, title: "READ ME FIRST: Admin Quick Start Guide", skip_validations: true, category: staff ? staff.name : nil) + filename = DiscoursePluginRegistry.seed_data["admin_quick_start_filename"] + if filename.nil? || !File.exists?(filename) + filename = Rails.root + 'docs/ADMIN-QUICK-START-GUIDE.md' + end + + welcome = File.read(filename) + PostCreator.create( Discourse.system_user, + raw: welcome, + title: DiscoursePluginRegistry.seed_data["admin_quick_start_title"] || "READ ME FIRST: Admin Quick Start Guide", + skip_validations: true, + category: staff ? staff.name : nil) end diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index af605b2770..8875807f5a 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -13,6 +13,7 @@ class DiscoursePluginRegistry attr_writer :sass_variables attr_writer :handlebars attr_writer :serialized_current_user_fields + attr_writer :seed_data attr_accessor :custom_html @@ -56,6 +57,10 @@ class DiscoursePluginRegistry def serialized_current_user_fields @serialized_current_user_fields ||= Set.new end + + def seed_data + @seed_data ||= HashWithIndifferentAccess.new({}) + end end def register_js(filename, options={}) @@ -104,6 +109,10 @@ class DiscoursePluginRegistry end end + def self.register_seed_data(key, value) + self.seed_data[key] = value + end + def javascripts self.class.javascripts end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 42b2e8bf96..99e978f9b9 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -17,6 +17,10 @@ class Plugin::Instance } end + def seed_data + @seed_data ||= {} + end + def self.find_all(parent_path) [].tap { |plugins| # also follows symlinks - http://stackoverflow.com/q/357754 @@ -166,7 +170,11 @@ class Plugin::Instance def register_color_scheme(name, colors) color_schemes << {name: name, colors: colors} - end + end + + def register_seed_data(key, value) + seed_data[key] = value + end def automatic_assets css = styles.join("\n") @@ -224,6 +232,10 @@ class Plugin::Instance register_assets! unless assets.blank? + seed_data.each do |key, value| + DiscoursePluginRegistry.register_seed_data(key, value) + end + # TODO: possibly amend this to a rails engine # Automatically include assets diff --git a/spec/components/discourse_plugin_registry_spec.rb b/spec/components/discourse_plugin_registry_spec.rb index 1171292676..98c5709ac1 100644 --- a/spec/components/discourse_plugin_registry_spec.rb +++ b/spec/components/discourse_plugin_registry_spec.rb @@ -43,6 +43,14 @@ describe DiscoursePluginRegistry do end end + context '#seed_data' do + it 'defaults to an empty Set' do + registry.seed_data = nil + expect(registry.seed_data).to be_a(Hash) + expect(registry.seed_data.size).to eq(0) + end + end + context '.register_css' do before do registry_instance.register_css('hello.css') @@ -143,4 +151,20 @@ describe DiscoursePluginRegistry do end end + context '#register_seed_data' do + let(:registry) { DiscoursePluginRegistry } + + after do + registry.reset! + end + + it "registers seed data properly" do + registry.register_seed_data("admin_quick_start_title", "Banana Hosting: Quick Start Guide") + registry.register_seed_data("admin_quick_start_filename", File.expand_path("../docs/BANANA-QUICK-START.md", __FILE__)) + + expect(registry.seed_data["admin_quick_start_title"]).to eq("Banana Hosting: Quick Start Guide") + expect(registry.seed_data["admin_quick_start_filename"]).to eq(File.expand_path("../docs/BANANA-QUICK-START.md", __FILE__)) + end + end + end From 4d593d1c186326ed6c4733a3bbe7cf1de04f62fd Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 5 Jun 2015 10:22:41 +0530 Subject: [PATCH 07/60] FIX: staff should be immune to max_invites_per_day setting --- lib/guardian.rb | 4 ++-- spec/components/guardian_spec.rb | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/guardian.rb b/lib/guardian.rb index 06518f6afe..6cc1e9e3fb 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -200,7 +200,7 @@ class Guardian def can_invite_to_forum?(groups=nil) authenticated? && - SiteSetting.max_invites_per_day.to_i > 0 && + (SiteSetting.max_invites_per_day.to_i > 0 || is_staff?) && !SiteSetting.enable_sso && SiteSetting.enable_local_logins && ( @@ -213,8 +213,8 @@ class Guardian def can_invite_to?(object, group_ids=nil) return false if ! authenticated? return false unless ( SiteSetting.enable_local_logins && (!SiteSetting.must_approve_users? || is_staff?) ) - return false if SiteSetting.max_invites_per_day.to_i == 0 return true if is_admin? + return false if (SiteSetting.max_invites_per_day.to_i == 0 && !is_staff?) return false if ! can_see?(object) return false if group_ids.present? diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index d5e6a8c137..8727666490 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -248,7 +248,10 @@ describe Guardian do it 'returns false when max_invites_per_day is 0' do # let's also break it while here SiteSetting.max_invites_per_day = "a" - expect(Guardian.new(moderator).can_invite_to_forum?).to be_falsey + + expect(Guardian.new(user).can_invite_to_forum?).to be_falsey + # staff should be immune to max_invites_per_day setting + expect(Guardian.new(moderator).can_invite_to_forum?).to be_truthy end it 'returns false when the site requires approving users and is regular' do @@ -283,7 +286,10 @@ describe Guardian do expect(Guardian.new(user).can_invite_to?(topic)).to be_falsey SiteSetting.max_invites_per_day = 0 - expect(Guardian.new(moderator).can_invite_to?(topic)).to be_falsey + + expect(Guardian.new(user).can_invite_to?(topic)).to be_falsey + # staff should be immune to max_invites_per_day setting + expect(Guardian.new(moderator).can_invite_to?(topic)).to be_truthy end it 'returns true when the site requires approving users and is mod' do From 4409a3072d864af1b0b0864e426d1efff526823d Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 5 Jun 2015 18:43:59 +1000 Subject: [PATCH 08/60] FEATURE: we need admin login always --- app/controllers/users_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4dd706cbb9..13bf9b2821 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -345,7 +345,7 @@ class UsersController < ApplicationController end def admin_login - unless SiteSetting.enable_sso && !current_user + if current_user return redirect_to path("/") end From 73646184aaa5a7a8dba24e7862af181817b06b51 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 5 Jun 2015 18:58:20 +1000 Subject: [PATCH 09/60] correct specs --- spec/controllers/users_controller_spec.rb | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 5388bb1f08..cf657684ea 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -321,22 +321,9 @@ describe UsersController do context 'enqueues mail' do it 'enqueues mail with admin email and sso enabled' do - SiteSetting.enable_sso = true Jobs.expects(:enqueue).with(:user_email, has_entries(type: :admin_login, user_id: admin.id)) put :admin_login, email: admin.email end - - it 'does not enqueue mail with admin email and sso disabled' do - SiteSetting.enable_sso = false - Jobs.expects(:enqueue).never - put :admin_login, email: admin.email - end - - it 'does not enqueue mail with normal user email and sso enabled' do - SiteSetting.enable_sso = true - Jobs.expects(:enqueue).never - put :admin_login, email: user.email - end end context 'logs in admin' do @@ -346,13 +333,13 @@ describe UsersController do expect(session[:current_user_id]).to be_blank end - it 'does not log in admin with valid token and SSO disabled' do + it 'does log in admin with valid token and SSO disabled' do SiteSetting.enable_sso = false token = admin.email_tokens.create(email: admin.email).token get :admin_login, token: token expect(response).to redirect_to('/') - expect(session[:current_user_id]).to be_blank + expect(session[:current_user_id]).to eq(admin.id) end it 'logs in admin with valid token and SSO enabled' do From afb566260e3a13c4f12141eafb93d4d803bfe3d6 Mon Sep 17 00:00:00 2001 From: cpradio Date: Fri, 5 Jun 2015 08:04:04 -0400 Subject: [PATCH 10/60] FEATURE: Use created_at to remove an ip if its last_match_at is null --- app/jobs/scheduled/clean_up_unmatched_ips.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/scheduled/clean_up_unmatched_ips.rb b/app/jobs/scheduled/clean_up_unmatched_ips.rb index 1e224ed7b7..abd3cdc442 100644 --- a/app/jobs/scheduled/clean_up_unmatched_ips.rb +++ b/app/jobs/scheduled/clean_up_unmatched_ips.rb @@ -11,7 +11,7 @@ module Jobs # remove old unmatched IP addresses ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block]) - .where("last_match_at < ?", last_match_threshold) + .where("last_match_at < ? OR (last_match_at IS NULL AND created_at < ?)", last_match_threshold, last_match_threshold) .destroy_all end From c6cd1928be34626626a9f5db9991212f2e12dd2e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 5 Jun 2015 11:46:21 -0400 Subject: [PATCH 11/60] Simple "cook" for email imports from mailing lists --- Gemfile.lock | 2 +- app/models/post.rb | 27 +++++++++++++++---------- lib/email_cook.rb | 36 +++++++++++++++++++++++++++++++++ lib/post_creator.rb | 1 + script/import_scripts/nabble.rb | 13 ++++++++---- 5 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 lib/email_cook.rb diff --git a/Gemfile.lock b/Gemfile.lock index ff5e238b0f..562f3a9197 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -486,4 +486,4 @@ DEPENDENCIES unicorn BUNDLED WITH - 1.10.2 + 1.10.3 diff --git a/app/models/post.rb b/app/models/post.rb index 281b6a3d4e..0dbe2c6c3b 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -6,6 +6,7 @@ require_dependency 'enum' require_dependency 'post_analyzer' require_dependency 'validators/post_validator' require_dependency 'plugin/filter' +require_dependency 'email_cook' require 'archetype' require 'digest/sha1' @@ -76,7 +77,7 @@ class Post < ActiveRecord::Base end def self.cook_methods - @cook_methods ||= Enum.new(:regular, :raw_html) + @cook_methods ||= Enum.new(:regular, :raw_html, :email) end def self.find_by_detail(key, value) @@ -161,16 +162,20 @@ class Post < ActiveRecord::Base # case we can skip the rendering pipeline. return raw if cook_method == Post.cook_methods[:raw_html] - # Default is to cook posts - cooked = if !self.user || SiteSetting.tl3_links_no_follow || !self.user.has_trust_level?(TrustLevel[3]) - post_analyzer.cook(*args) - else - # At trust level 3, we don't apply nofollow to links - cloned = args.dup - cloned[1] ||= {} - cloned[1][:omit_nofollow] = true - post_analyzer.cook(*cloned) - end + cooked = nil + if cook_method == Post.cook_methods[:email] + cooked = EmailCook.new(raw).cook + else + cooked = if !self.user || SiteSetting.tl3_links_no_follow || !self.user.has_trust_level?(TrustLevel[3]) + post_analyzer.cook(*args) + else + # At trust level 3, we don't apply nofollow to links + cloned = args.dup + cloned[1] ||= {} + cloned[1][:omit_nofollow] = true + post_analyzer.cook(*cloned) + end + end new_cooked = Plugin::Filter.apply(:after_post_cook, self, cooked) diff --git a/lib/email_cook.rb b/lib/email_cook.rb new file mode 100644 index 0000000000..33693190cb --- /dev/null +++ b/lib/email_cook.rb @@ -0,0 +1,36 @@ +# A very simple formatter for imported emails +class EmailCook + + def initialize(raw) + @raw = raw + end + + def cook + result = "" + + in_quote = false + quote_buffer = "" + @raw.each_line do |l| + + if l =~ /^\s*>/ + in_quote = true + quote_buffer << l.sub(/^[\s>]*/, '') << "
" + elsif in_quote + result << "
#{quote_buffer}
" + quote_buffer = "" + in_quote = false + else + result << l << "
" + end + end + + if in_quote + result << "
#{quote_buffer}
" + end + + result.gsub!(/(
){3,10}/, '

') + + result + end + +end diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 0dc39952ee..bb9723a959 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -28,6 +28,7 @@ class PostCreator # cook_method - Method of cooking the post. # :regular - Pass through Markdown parser and strip bad HTML # :raw_html - Perform no processing + # :raw_email - Imported from an email # via_email - Mark this post as arriving via email # raw_email - Full text of arriving email (to store) # diff --git a/script/import_scripts/nabble.rb b/script/import_scripts/nabble.rb index c975b5cf1b..35eceb146f 100644 --- a/script/import_scripts/nabble.rb +++ b/script/import_scripts/nabble.rb @@ -54,7 +54,10 @@ class ImportScripts::MyAskBot < ImportScripts::Base def parse_email(msg) receiver = Email::Receiver.new(msg, skip_sanity_check: true) mail = Mail.read_from_string(msg) - receiver.parse_body(mail) + mail.body + + selected = receiver.select_body(mail) + selected.force_encoding(selected.encoding).encode("UTF-8") end def create_forum_topics @@ -86,7 +89,8 @@ class ImportScripts::MyAskBot < ImportScripts::Base user_id: user_id_from_imported_user_id(t["owner_id"]) || Discourse::SYSTEM_USER_ID, created_at: Time.zone.at(@td.decode(t["when_created"])), category: CATEGORY_ID, - raw: raw } + raw: raw, + cook_method: Post.cook_methods[:email] } end end end @@ -137,10 +141,11 @@ class ImportScripts::MyAskBot < ImportScripts::Base topic_id: topic_id, user_id: user_id_from_imported_user_id(p['owner_id']) || Discourse::SYSTEM_USER_ID, created_at: Time.zone.at(@td.decode(p["when_created"])), - raw: raw } + raw: raw, + cook_method: Post.cook_methods[:email] } end end end end -ImportScripts::MyAskBot.new.perform +ImportScripts::MyAskBot.new.perform From 6000a37ff1b127480b96776b32749e2960d223d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 5 Jun 2015 18:37:34 +0200 Subject: [PATCH 12/60] new 'uploads:migrate_to_new_pattern' task --- lib/tasks/uploads.rake | 174 ++++++++++++++++++++++++----------------- 1 file changed, 101 insertions(+), 73 deletions(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index fcf99f2865..8ff0c70e0c 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -369,6 +369,9 @@ end ################################################################################ task "uploads:migrate_to_new_pattern" => :environment do + require "file_helper" + require "file_store/local_store" + ENV["RAILS_DB"] ? migrate_to_new_pattern : migrate_to_new_pattern_all_sites end @@ -389,44 +392,59 @@ def migrate_to_new_pattern end def migrate_uploads_to_new_pattern - if Upload.where(sha1: nil).exists? - puts "Computing missing SHAs..." - - Upload.where(sha1: nil).find_each do |upload| - path = Discourse.store.path_for(upload) - size = File.size(path) rescue 0 - if size > 0 - upload.sha1 = Digest::SHA1.file(path).hexdigest - upload.save - putc "." - else - upload.destroy - putc "X" - end - end - - puts - end - puts "Moving uploads to new location..." - Upload.where.not(sha1: nil) - .where("url LIKE '/uploads/%'") - .where("url NOT LIKE '/uploads/%/original/%'") - .find_each do |upload| - path = Discourse.store.path_for(upload) - if File.exists?(path) - file = File.open(path) - # copy file to new location - url = Discourse.store.store_upload(file, upload) - file.try(:close!) rescue nil - # remap URLs - remap(upload.url, url) - # remove old file - FileUtils.rm(path, force: true) rescue nil - putc "." - else - # upload.destroy - putc "X" + + max_file_size_kb = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes + local_store = FileStore::LocalStore.new + + Upload.where("LENGTH(COALESCE(url, '')) = 0").destroy_all + + Upload.where("url NOT LIKE '%/original/_X/%'").find_each do |upload| + begin + successful = false + # keep track of the url + previous_url = upload.url.dup + # where is the file currently stored? + external = previous_url =~ /^\/\// + # download if external + if external + url = SiteSetting.scheme + ":" + previous_url + file = FileHelper.download(url, max_file_size_kb, "discourse", true) rescue nil + next unless file + path = file.path + else + path = local_store.path_for(upload) + next unless File.exists?(path) + end + # compute SHA if missing + if upload.sha1.blank? + upload.sha1 = Digest::SHA1.file(path).hexdigest + end + # optimize if image + if FileHelper.is_image?(File.basename(path)) + ImageOptim.new.optimize_image!(path) + end + # store to new location & update the filesize + File.open(path) do |f| + upload.url = Discourse.store.store_upload(f, upload) + upload.filesize = f.size + upload.save + end + # remap the URLs + remap(previous_url, upload.url) + # remove the old file (when local) + unless external + FileUtils.rm(path, force: true) rescue nil + end + # succesfully migrated + successful = true + rescue => e + puts e.message + puts e.backtrace.join("\n") + ensure + putc successful ? '.' : 'X' + file.try(:unlink) rescue nil + file.try(:close) rescue nil end end @@ -434,45 +452,55 @@ def migrate_uploads_to_new_pattern end def migrate_optimized_images_to_new_pattern - if OptimizedImage.where(sha1: nil).exists? - puts "Computing missing SHAs..." + max_file_size_kb = SiteSetting.max_image_size_kb.kilobytes + local_store = FileStore::LocalStore.new - OptimizedImage.where(sha1: nil).find_each do |optimized_image| - path = Discourse.store.path_for(optimized_image) - size = File.size(path) rescue 0 - if size > 0 - optimized_image.sha1 = Digest::SHA1.file(path).hexdigest - optimized_image.save - putc "." + OptimizedImage.where("LENGTH(COALESCE(url, '')) = 0").destroy_all + + OptimizedImage.where("url NOT LIKE '%/original/_X/%'").find_each do |optimized_image| + begin + successful = false + # keep track of the url + previous_url = optimized_image.url.dup + # where is the file currently stored? + external = previous_url =~ /^\/\// + # download if external + if external + url = SiteSetting.scheme + ":" + previous_url + file = FileHelper.download(url, max_file_size_kb, "discourse", true) rescue nil + next unless file + path = file.path else - optimized_image.destroy - putc "X" + path = local_store.path_for(optimized_image) + next unless File.exists?(path) + file = File.open(path) end - end - - puts - end - - puts "Moving optimized images to new location..." - OptimizedImage.where.not(sha1: nil) - .where("width > 0 AND height > 0") - .where("url LIKE '/uploads/%/_optimized/%'") - .where("url NOT LIKE '/uploads/%/optimized/%'") - .find_each do |optimized_image| - path = Discourse.store.path_for(optimized_image) - if File.exists?(path) - file = File.open(path) - # copy file to new location - url = Discourse.store.store_optimized_image(file, optimized_image) - file.try(:close!) rescue nil - # remap URLs - remap(optimized_image.url, url) - # remove old file - FileUtils.rm(path, force: true) rescue nil - putc "." - else - optimized_image.destroy - putc "X" + # compute SHA if missing + if optimized_image.sha1.blank? + optimized_image.sha1 = Digest::SHA1.file(path).hexdigest + end + # optimize if image + ImageOptim.new.optimize_image!(path) + # store to new location & update the filesize + File.open(path) do |f| + optimized_image.url = Discourse.store.store_optimized_image(f, optimized_image) + optimized_image.save + end + # remap the URLs + remap(previous_url, optimized_image.url) + # remove the old file (when local) + unless external + FileUtils.rm(path, force: true) rescue nil + end + # succesfully migrated + successful = true + rescue => e + puts e.message + puts e.backtrace.join("\n") + ensure + putc successful ? '.' : 'X' + file.try(:unlink) rescue nil + file.try(:close) rescue nil end end From 4171eb758c29e0adc5c5af6e968c73f88d34ce0e Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 6 Jun 2015 03:09:02 +1000 Subject: [PATCH 13/60] SECURITY: expire all existing sessions if user changes passwords --- app/models/user.rb | 5 ++++- spec/models/user_spec.rb | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 0b528cbefe..a55a3e754c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -320,7 +320,10 @@ class User < ActiveRecord::Base def password=(password) # special case for passwordless accounts - @raw_password = password unless password.blank? + unless password.blank? + @raw_password = password + self.auth_token = nil + end end def password diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9e676b1494..a204083e08 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -509,18 +509,24 @@ describe User do end describe 'passwords' do - before do + + it "should not have an active account with a good password" do @user = Fabricate.build(:user, active: false) @user.password = "ilovepasta" @user.save! - end - it "should have a valid password after the initial save" do - expect(@user.confirm_password?("ilovepasta")).to eq(true) - end + @user.auth_token = SecureRandom.hex(16) + @user.save! - it "should not have an active account after initial save" do expect(@user.active).to eq(false) + expect(@user.confirm_password?("ilovepasta")).to eq(true) + + old_token = @user.auth_token + @user.password = "passwordT" + @user.save! + + # must expire old token on password change + expect(@user.auth_token).to_not eq(old_token) end end From feeb509a97d440580233127a345729dbae592ffb Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Sat, 6 Jun 2015 03:50:06 +1000 Subject: [PATCH 14/60] SECURITY: expire all existing email tokens on password reset --- app/models/user.rb | 7 +++++++ spec/models/user_spec.rb | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/app/models/user.rb b/app/models/user.rb index a55a3e754c..ca45d6b6f4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -92,6 +92,7 @@ class User < ActiveRecord::Base after_save :clear_global_notice_if_needed after_save :refresh_avatar after_save :badge_grant + after_save :expire_old_email_tokens before_destroy do # These tables don't have primary keys, so destroying them with activerecord is tricky: @@ -786,6 +787,12 @@ class User < ActiveRecord::Base BadgeGranter.queue_badge_grant(Badge::Trigger::UserChange, user: self) end + def expire_old_email_tokens + if password_hash_changed? && !id_changed? + email_tokens.where('not expired').update_all(expired: true) + end + end + def update_tracked_topics return unless auto_track_topics_after_msecs_changed? TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a204083e08..bd100e9fa3 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -521,12 +521,18 @@ describe User do expect(@user.active).to eq(false) expect(@user.confirm_password?("ilovepasta")).to eq(true) + + email_token = @user.email_tokens.create(email: 'pasta@delicious.com') + old_token = @user.auth_token @user.password = "passwordT" @user.save! # must expire old token on password change expect(@user.auth_token).to_not eq(old_token) + + email_token.reload + expect(email_token.expired).to eq(true) end end From a09d893c0cd61bf3f37bbcb36bf1085a29f7db5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 5 Jun 2015 20:24:49 +0200 Subject: [PATCH 15/60] FIX: missing emoji autocomplete --- app/assets/javascripts/discourse/lib/emoji/emoji.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb b/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb index 9f82adde00..582af89dfe 100644 --- a/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb +++ b/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb @@ -166,7 +166,7 @@ Discourse.Emoji.search = function(term, options) { var maxResults = (options && options["maxResults"]) || -1; if (maxResults === 0) { return []; } - toSearch = toSearch || _.merge(_.keys(emojiHash), _.keys(extendedEmoji)); + toSearch = toSearch || _.union(_.keys(emojiHash), _.keys(extendedEmoji)).sort(); var i, results = []; From 74141cc475945568bb165bc9dad05a284d4ef7ae Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 8 Jun 2015 11:09:38 +0530 Subject: [PATCH 16/60] FIX: send 404 error when unauthorized user tries to download user archive --- app/controllers/export_csv_controller.rb | 2 +- spec/controllers/export_csv_controller_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/export_csv_controller.rb b/app/controllers/export_csv_controller.rb index af53d963ba..74e341168c 100644 --- a/app/controllers/export_csv_controller.rb +++ b/app/controllers/export_csv_controller.rb @@ -20,7 +20,7 @@ class ExportCsvController < ApplicationController export_initiated_by_user_id = UserExport.where(id: export_id)[0].user_id unless UserExport.where(id: export_id).empty? export_csv_path = UserExport.get_download_path(filename) - if export_csv_path && export_initiated_by_user_id == current_user.id + if export_csv_path && current_user.present? && export_initiated_by_user_id == current_user.id send_file export_csv_path else render nothing: true, status: 404 diff --git a/spec/controllers/export_csv_controller_spec.rb b/spec/controllers/export_csv_controller_spec.rb index 9975b2cf29..440ccc11ef 100644 --- a/spec/controllers/export_csv_controller_spec.rb +++ b/spec/controllers/export_csv_controller_spec.rb @@ -3,6 +3,14 @@ require "spec_helper" describe ExportCsvController do let(:export_filename) { "user-archive-codinghorror-150115-234817-999.csv.gz" } + context "while not logged in" do + describe ".download" do + it "returns 404 when the unauthorized user tries to export csv file" do + get :show, id: export_filename + expect(response.status).to eq(404) + end + end + end context "while logged in as normal user" do before { @user = log_in(:user) } From b9938fc96991d10636d9c6cb71221a50d185a416 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 8 Jun 2015 18:11:37 +1000 Subject: [PATCH 17/60] FEATURE: automatically detect and correct message bus subscription failures --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 562f3a9197..e7b6d30551 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -153,7 +153,7 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) memory_profiler (0.9.3) - message_bus (1.0.12) + message_bus (1.0.13) rack (>= 1.1.3) redis metaclass (0.0.4) From 5da5269652045311fc8d4cc6b0a9549bbfd590dd Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 8 Jun 2015 12:07:35 -0400 Subject: [PATCH 18/60] FIX: Bad page title for categories view by google crawler --- app/controllers/application_controller.rb | 4 ++++ app/controllers/categories_controller.rb | 4 ++++ app/controllers/list_controller.rb | 3 +-- app/views/categories/index.html.erb | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8ec435424f..7b3534046a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -212,6 +212,10 @@ class ApplicationController < ActionController::Base @guardian ||= Guardian.new(current_user) end + def current_homepage + current_user ? SiteSetting.homepage : SiteSetting.anonymous_homepage + end + def serialize_data(obj, serializer, opts=nil) # If it's an array, apply the serializer as an each_serializer to the elements serializer_opts = {scope: guardian}.merge!(opts || {}) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 0fa958c8ee..8199472107 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -24,6 +24,10 @@ class CategoriesController < ApplicationController discourse_expires_in 1.minute + unless current_homepage == 'categories' + @title = I18n.t('js.filters.categories.title') + end + store_preloaded("categories_list", MultiJson.dump(CategoryListSerializer.new(@list, scope: guardian))) respond_to do |format| format.html { render } diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index e6e30a54c1..927519cb49 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -60,7 +60,6 @@ class ListController < ApplicationController list_opts[:no_definitions] = true end - list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") list.more_topics_url = construct_url_with(:next, list_opts) list.prev_topics_url = construct_url_with(:prev, list_opts) @@ -69,7 +68,7 @@ class ListController < ApplicationController @rss = filter # Note the first is the default and we don't add a title - if idx > 0 && use_crawler_layout? + if (filter.to_s != current_homepage) && use_crawler_layout? filter_title = I18n.t("js.filters.#{filter.to_s}.title") if list_opts[:category] @title = I18n.t('js.filters.with_category', filter: filter_title, category: Category.find(list_opts[:category]).name) diff --git a/app/views/categories/index.html.erb b/app/views/categories/index.html.erb index 142bc7d0ab..f3ef3b7d6a 100644 --- a/app/views/categories/index.html.erb +++ b/app/views/categories/index.html.erb @@ -23,4 +23,4 @@ <% end %> -<% content_for :title do %><%= I18n.t('js.filters.categories.title') %><% end %> +<% content_for :title do %><%= @title %><% end %> From 545f19500dd1f80f27b4bd78c88ed124b2a3a144 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 9 Jun 2015 13:05:26 +0530 Subject: [PATCH 19/60] FIX: when sending private message emails do not check email_direct setting --- app/models/user_email_observer.rb | 4 ++-- spec/models/user_email_observer_spec.rb | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/models/user_email_observer.rb b/app/models/user_email_observer.rb index e2389f5bb4..3f24c6c568 100644 --- a/app/models/user_email_observer.rb +++ b/app/models/user_email_observer.rb @@ -41,7 +41,6 @@ class UserEmailObserver < ActiveRecord::Observer def enqueue(type) return unless notification.user.email_direct? - Jobs.enqueue_in(delay, :user_email, type: type, @@ -50,7 +49,8 @@ class UserEmailObserver < ActiveRecord::Observer end def enqueue_private(type) - return unless (notification.user.email_direct? && notification.user.email_private_messages?) + return unless notification.user.email_private_messages? + Jobs.enqueue_in(delay, :user_email, type: type, diff --git a/spec/models/user_email_observer_spec.rb b/spec/models/user_email_observer_spec.rb index 20c632e129..ece539e7d2 100644 --- a/spec/models/user_email_observer_spec.rb +++ b/spec/models/user_email_observer_spec.rb @@ -98,6 +98,24 @@ describe UserEmailObserver do end + context 'private_message' do + + let(:user) { Fabricate(:user) } + let!(:notification) { Fabricate(:notification, user: user, notification_type: 6) } + + it "enqueues a job for the email" do + Jobs.expects(:enqueue_in).with(SiteSetting.email_time_window_mins.minutes, :user_email, type: :user_private_message, user_id: notification.user_id, notification_id: notification.id) + UserEmailObserver.send(:new).after_commit(notification) + end + + it "doesn't enqueue an email if the user has private message emails disabled" do + user.expects(:email_private_messages?).returns(false) + Jobs.expects(:enqueue_in).with(SiteSetting.email_time_window_mins.minutes, :user_email, has_entry(type: :user_private_message)).never + UserEmailObserver.send(:new).after_commit(notification) + end + + end + context 'user_invited_to_topic' do let(:user) { Fabricate(:user) } From 64c0c968e468f34cbf1d5e9dad85cd6d301d72e2 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 9 Jun 2015 18:48:06 +1000 Subject: [PATCH 20/60] Attempt micro data using old vocubulary Seeing weird results on Google --- app/views/topics/show.html.erb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/app/views/topics/show.html.erb b/app/views/topics/show.html.erb index 9fd1fc836c..76404ccfa7 100644 --- a/app/views/topics/show.html.erb +++ b/app/views/topics/show.html.erb @@ -4,14 +4,16 @@ <% @breadcrumbs = categories_breadcrumb(@topic_view.topic) if @breadcrumbs.present? %> -
+ <% end %> From c872e3f092859136ce1ce29be7864f3b1a459fa1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 9 Jun 2015 20:34:06 +0800 Subject: [PATCH 21/60] Show breakdown for site settings. --- .../javascripts/admin/controllers/admin-site-settings.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 index 9e67e638b5..ae97f0f0f2 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 @@ -39,6 +39,11 @@ export default Ember.ArrayController.extend(Presence, { }); if (matches.length > 0) { matchesGroupedByCategory[0].siteSettings.pushObjects(matches); + matchesGroupedByCategory.pushObject({ + nameKey: settingsCategory.nameKey, + name: I18n.t('admin.site_settings.categories.' + settingsCategory.nameKey), + siteSettings: matches + }); } }); From 5db768b69f758e98f230315505c6340dffecf7e6 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 10 Jun 2015 00:28:01 +1000 Subject: [PATCH 22/60] FEATURE: improve no-js topic list information - Provide links to pages - Provied link to last topic --- app/helpers/list_helper.rb | 20 ++++++++++++++++++++ app/views/list/list.erb | 3 ++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 app/helpers/list_helper.rb diff --git a/app/helpers/list_helper.rb b/app/helpers/list_helper.rb new file mode 100644 index 0000000000..a7cf7766ac --- /dev/null +++ b/app/helpers/list_helper.rb @@ -0,0 +1,20 @@ +module ListHelper + def page_links(topic) + posts = topic.posts_count + max_pages = 10 + total_pages = (posts / TopicView.chunk_size) + (posts == TopicView.chunk_size ? 0 : 1) + + return if total_pages < 2 + + page = [total_pages - (max_pages+1), 2].max + + result = "(" + while page <= total_pages + result << " #{page} " + page += 1 + end + + result << ")" + result.html_safe + end +end diff --git a/app/views/list/list.erb b/app/views/list/list.erb index 81663c7d86..7382cea1c0 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -21,10 +21,11 @@ <%= t.title %> + <%= page_links(t) %> <% if !@category && t.category %> [<%= t.category.name %>] <% end %> - '>(<%= t.posts_count %>) + '>(<%= t.posts_count %>)
<% end %> From 71ee84f84842590fefbef982ea7e6e20b7220bfc Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 9 Jun 2015 17:09:03 +0530 Subject: [PATCH 23/60] FEATURE: latest posts RSS feed --- app/controllers/posts_controller.rb | 28 +++++++++++++++++++--------- app/views/posts/latest.rss.erb | 21 +++++++++++++++++++++ config/locales/server.en.yml | 1 + 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 app/views/posts/latest.rss.erb diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index f595ada867..ad0e8cb540 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -9,7 +9,7 @@ class PostsController < ApplicationController # Need to be logged in for all actions here before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :reply_history, :revisions, :latest_revision, :expand_embed, :markdown_id, :markdown_num, :cooked, :latest] - skip_before_filter :preload_json, :check_xhr, only: [:markdown_id, :markdown_num, :short_link] + skip_before_filter :preload_json, :check_xhr, only: [:markdown_id, :markdown_num, :short_link, :latest] def markdown_id markdown Post.find(params[:id].to_i) @@ -42,16 +42,26 @@ class PostsController < ApplicationController # Remove posts the user doesn't have permission to see # This isn't leaking any information we weren't already through the post ID numbers posts = posts.reject { |post| !guardian.can_see?(post) } - counts = PostAction.counts_for(posts, current_user) - render_json_dump(serialize_data(posts, - PostSerializer, - scope: guardian, - root: 'latest_posts', - add_raw: true, - all_post_actions: counts) - ) + respond_to do |format| + format.rss do + @posts = posts + @title = "#{SiteSetting.title} - #{I18n.t("rss_description.posts")}" + @link = Discourse.base_url + @description = I18n.t("rss_description.posts") + render 'posts/latest', formats: [:rss] + end + format.json do + render_json_dump(serialize_data(posts, + PostSerializer, + scope: guardian, + root: 'latest_posts', + add_raw: true, + all_post_actions: counts) + ) + end + end end def cooked diff --git a/app/views/posts/latest.rss.erb b/app/views/posts/latest.rss.erb new file mode 100644 index 0000000000..04aeb02787 --- /dev/null +++ b/app/views/posts/latest.rss.erb @@ -0,0 +1,21 @@ + + + + <% lang = SiteSetting.find_by_name('default_locale').try(:value) %> + <% site_email = SiteSetting.find_by_name('contact_email').try(:value) %> + <%= @title %> + <%= @link %> + <%= @description %> + <% @posts.each do |post| %> + <% next unless post.user %> + + <%= post.topic.title %> + <%= "no-reply@example.com (@#{post.user.username}#{" #{post.user.name}" if (post.user.name.present? && SiteSetting.enable_names?)})" -%> + ]]> + <%= Discourse.base_url + post.url %> + <%= post.created_at.rfc2822 %> + post-<%= post.id %> + + <% end %> + + diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 9ca22fef33..fc8525206f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -173,6 +173,7 @@ en: rss_description: latest: "Latest topics" hot: "Hot topics" + posts: "Latest posts" too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted." excerpt_image: "image" From 7b6d6b76eb3b7dbf1982c382a1f0e981e7ff8eee Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 9 Jun 2015 12:19:41 -0400 Subject: [PATCH 24/60] FEATURE: Multiple embeddable hosts - Also refactors two site settings components into one, with tests --- .../admin/components/site-setting.js.es6 | 2 +- .../admin/components/url-list.js.es6 | 32 ----------------- .../admin/components/value-list.js.es6 | 34 +++++++++++++++++++ .../admin/templates/components/url-list.hbs | 18 ---------- .../admin/templates/components/value-list.hbs | 18 ++++++++++ .../templates/site-settings/host_list.hbs | 3 ++ .../templates/site-settings/url_list.hbs | 2 +- .../stylesheets/common/admin/admin_base.scss | 6 ++-- app/controllers/embed_controller.rb | 4 +-- app/models/site_setting.rb | 13 +++++-- app/models/topic.rb | 2 +- config/locales/client.en.yml | 1 + config/locales/server.bs_BA.yml | 2 +- config/locales/server.de.yml | 2 +- config/locales/server.en.yml | 2 +- config/locales/server.es.yml | 2 +- config/locales/server.fa_IR.yml | 2 +- config/locales/server.fi.yml | 2 +- config/locales/server.fr.yml | 2 +- config/locales/server.he.yml | 2 +- config/locales/server.ja.yml | 2 +- config/locales/server.pt.yml | 2 +- config/locales/server.pt_BR.yml | 2 +- config/locales/server.ru.yml | 2 +- config/locales/server.sq.yml | 2 +- config/locales/server.tr_TR.yml | 2 +- config/locales/server.zh_CN.yml | 2 +- config/site_settings.yml | 6 ++-- .../20150609163211_migrate_embeddable_host.rb | 5 +++ lib/site_setting_extension.rb | 2 +- lib/topic_retriever.rb | 5 +-- spec/controllers/embed_controller_spec.rb | 30 +++++++++++++--- spec/models/site_setting_spec.rb | 33 ++++++++++++------ spec/models/topic_spec.rb | 8 ++--- .../components/value-list-test.js.es6 | 31 +++++++++++++++++ 35 files changed, 183 insertions(+), 102 deletions(-) delete mode 100644 app/assets/javascripts/admin/components/url-list.js.es6 create mode 100644 app/assets/javascripts/admin/components/value-list.js.es6 delete mode 100644 app/assets/javascripts/admin/templates/components/url-list.hbs create mode 100644 app/assets/javascripts/admin/templates/components/value-list.hbs create mode 100644 app/assets/javascripts/admin/templates/site-settings/host_list.hbs create mode 100644 db/migrate/20150609163211_migrate_embeddable_host.rb create mode 100644 test/javascripts/components/value-list-test.js.es6 diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index f8e3cc6b2c..c7375af4fc 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -2,7 +2,7 @@ import BufferedContent from 'discourse/mixins/buffered-content'; import ScrollTop from 'discourse/mixins/scroll-top'; import SiteSetting from 'admin/models/site-setting'; -const CustomTypes = ['bool', 'enum', 'list', 'url_list']; +const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list']; export default Ember.Component.extend(BufferedContent, ScrollTop, { classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'], diff --git a/app/assets/javascripts/admin/components/url-list.js.es6 b/app/assets/javascripts/admin/components/url-list.js.es6 deleted file mode 100644 index 4562c4b20e..0000000000 --- a/app/assets/javascripts/admin/components/url-list.js.es6 +++ /dev/null @@ -1,32 +0,0 @@ -export default Ember.Component.extend({ - _setupUrls: function() { - const value = this.get('value'); - this.set('urls', (value && value.length) ? value.split("\n") : []); - }.on('init').observes('value'), - - _urlsChanged: function() { - this.set('value', this.get('urls').join("\n")); - }.observes('urls.@each'), - - urlInvalid: Ember.computed.empty('newUrl'), - - keyDown(e) { - if (e.keyCode === 13) { - this.send('addUrl'); - } - }, - - actions: { - addUrl() { - if (this.get('urlInvalid')) { return; } - - this.get('urls').addObject(this.get('newUrl')); - this.set('newUrl', ''); - }, - - removeUrl(url) { - const urls = this.get('urls'); - urls.removeObject(url); - } - } -}); diff --git a/app/assets/javascripts/admin/components/value-list.js.es6 b/app/assets/javascripts/admin/components/value-list.js.es6 new file mode 100644 index 0000000000..c995097a9d --- /dev/null +++ b/app/assets/javascripts/admin/components/value-list.js.es6 @@ -0,0 +1,34 @@ +export default Ember.Component.extend({ + classNameBindings: [':value-list'], + + _setupCollection: function() { + const values = this.get('values'); + this.set('collection', (values && values.length) ? values.split("\n") : []); + }.on('init').observes('values'), + + _collectionChanged: function() { + this.set('values', this.get('collection').join("\n")); + }.observes('collection.@each'), + + inputInvalid: Ember.computed.empty('newValue'), + + keyDown(e) { + if (e.keyCode === 13) { + this.send('addValue'); + } + }, + + actions: { + addValue() { + if (this.get('inputInvalid')) { return; } + + this.get('collection').addObject(this.get('newValue')); + this.set('newValue', ''); + }, + + removeValue(value) { + const collection = this.get('collection'); + collection.removeObject(value); + } + } +}); diff --git a/app/assets/javascripts/admin/templates/components/url-list.hbs b/app/assets/javascripts/admin/templates/components/url-list.hbs deleted file mode 100644 index d313c5b30a..0000000000 --- a/app/assets/javascripts/admin/templates/components/url-list.hbs +++ /dev/null @@ -1,18 +0,0 @@ -{{#if urls}} -
- {{#each url in urls}} -
- {{d-button action="removeUrl" - actionParam=url - icon="times" - class="btn-small no-text"}} - {{url}} -
- {{/each}} -
-{{/if}} - -
- {{text-field value=newUrl placeholderKey="admin.site_settings.add_url"}} - {{d-button action="addUrl" icon="plus" class="btn-primary btn-small no-text" disabled=urlInvalid}} -
diff --git a/app/assets/javascripts/admin/templates/components/value-list.hbs b/app/assets/javascripts/admin/templates/components/value-list.hbs new file mode 100644 index 0000000000..74904f92e8 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/value-list.hbs @@ -0,0 +1,18 @@ +{{#if collection}} +
+ {{#each collection as |value|}} +
+ {{d-button action="removeValue" + actionParam=value + icon="times" + class="btn-small no-text"}} + {{value}} +
+ {{/each}} +
+{{/if}} + +
+ {{text-field value=newValue placeholderKey=addKey}} + {{d-button action="addValue" icon="plus" class="btn-primary btn-small no-text" disabled=inputInvalid}} +
diff --git a/app/assets/javascripts/admin/templates/site-settings/host_list.hbs b/app/assets/javascripts/admin/templates/site-settings/host_list.hbs new file mode 100644 index 0000000000..5f0c301d0d --- /dev/null +++ b/app/assets/javascripts/admin/templates/site-settings/host_list.hbs @@ -0,0 +1,3 @@ +{{value-list values=buffered.value addKey="admin.site_settings.add_host"}} +{{setting-validation-message message=validationMessage}} +
{{{unbound setting.description}}}
diff --git a/app/assets/javascripts/admin/templates/site-settings/url_list.hbs b/app/assets/javascripts/admin/templates/site-settings/url_list.hbs index bd22e1b033..12e640c2c3 100644 --- a/app/assets/javascripts/admin/templates/site-settings/url_list.hbs +++ b/app/assets/javascripts/admin/templates/site-settings/url_list.hbs @@ -1,3 +1,3 @@ -{{url-list value=buffered.value}} +{{value-list value=buffered.value addKey="admin.site_settings.add_url"}} {{setting-validation-message message=validationMessage}}
{{{unbound setting.description}}}
diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 6659f61a7f..580ff074e5 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1449,8 +1449,8 @@ table#user-badges { } } -.url-list { - .url { +.value-list { + .value { border-bottom: 1px solid #ddd; padding: 3px; margin-right: 10px; @@ -1460,7 +1460,7 @@ table#user-badges { text-overflow: ellipsis; } - .urls { + .values { margin-bottom: 10px; } diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index 249a18f7a7..198288297a 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -53,8 +53,8 @@ class EmbedController < ApplicationController def ensure_embeddable if !(Rails.env.development? && current_user.try(:admin?)) - raise Discourse::InvalidAccess.new('embeddable host not set') if SiteSetting.normalized_embeddable_host.blank? - raise Discourse::InvalidAccess.new('invalid referer host') if URI(request.referer || '').host != SiteSetting.normalized_embeddable_host + raise Discourse::InvalidAccess.new('embeddable hosts not set') if SiteSetting.embeddable_hosts.blank? + raise Discourse::InvalidAccess.new('invalid referer host') unless SiteSetting.allows_embeddable_host?(request.referer) end response.headers['X-Frame-Options'] = "ALLOWALL" diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 5b272c0d91..02dad62554 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -68,9 +68,16 @@ class SiteSetting < ActiveRecord::Base @anonymous_menu_items ||= Set.new Discourse.anonymous_filters.map(&:to_s) end - def self.normalized_embeddable_host - return embeddable_host if embeddable_host.blank? - embeddable_host.sub(/^https?\:\/\//, '') + def self.allows_embeddable_host?(host) + return false if embeddable_hosts.blank? + uri = URI(host) rescue nil + + return false unless uri.present? + + host = uri.host + return false unless host.present? + + !!embeddable_hosts.split("\n").detect {|h| h.sub(/^https?\:\/\//, '') == host } end def self.anonymous_homepage diff --git a/app/models/topic.rb b/app/models/topic.rb index 1d0f6e4573..d9ebbd3af1 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -842,7 +842,7 @@ class Topic < ActiveRecord::Base end def expandable_first_post? - SiteSetting.embeddable_host.present? && SiteSetting.embed_truncate? && has_topic_embed? + SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed? end private diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4034dd8b64..f4a746ba60 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2292,6 +2292,7 @@ en: no_results: "No results found." clear_filter: "Clear" add_url: "add URL" + add_host: "add host" categories: all_results: 'All' required: 'Required' diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index e492bd653c..930e3ae556 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -738,7 +738,7 @@ bs_BA: default_code_lang: "Default programming language syntax highlighting applied to GitHub code blocks (lang-auto, ruby, python etc.)" warn_reviving_old_topic_age: "When someone starts replying to a topic where the last reply is older than this many days, a warning will be displayed. Disable by setting to 0." autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language." - embeddable_host: "Host that can embed the comments from this Discourse forum." + embeddable_hosts: "Host that can embed the comments from this Discourse forum." feed_polling_enabled: "EMBEDDING ONLY: Whether to embed a RSS/ATOM feed as posts." feed_polling_url: "EMBEDDING ONLY: URL of RSS/ATOM feed to embed." embed_by_username: "Discourse username of the user who creates the embedded topics." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index cf2a6222b4..9630552949 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -937,7 +937,7 @@ de: warn_reviving_old_topic_age: "Wenn jemand beginnt auf ein Thema zu antworten, dessen letzte Antwort älter als diese Anzahl an Tagen ist, wird eine Warnung angezeigt. Deaktiviere dies durch setzen auf 0." autohighlight_all_code: "Erzwinge Syntaxhervorhebung für alle Quellcode-Blöcke, auch dann wenn keine Sprache angeben wurde." highlighted_languages: "Es wurden Syntaxregeln zur Hervorhebung von Textstellen hinzugefügt. (Achtung: Werden zu viele Sprachen hinzugefügt, kann das die Performance beeinflussen) siehe: https://highlightjs.org/static/demo/ für eine Demo." - embeddable_host: "Host, der Kommentare aus diesem Discourse Forum einbetten darf. Nur Hostname ohne http://" + embeddable_hosts: "Host, der Kommentare aus diesem Discourse Forum einbetten darf. Nur Hostname ohne http://" feed_polling_enabled: "NUR WENN EINGEBETTET: Bestimmt, ob Inhalte eines RSS-/ATOM-Feeds als zusätzliche Beiträge dargestellt werden." feed_polling_url: "NUR WENN EINGEBETTET: URL des einzubettenden RSS-/ATOM-Feeds." embed_by_username: "Discourse-Benutzername des Benutzers, der die eingebetteten Themen erstellt." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index fc8525206f..bb6ce41067 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1126,7 +1126,7 @@ en: autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language." highlighted_languages: "Included syntax highlighting rules. (Warning: including too many langauges may impact performance) see: https://highlightjs.org/static/demo/ for a demo" - embeddable_host: "Host that can embed the comments from this Discourse forum. Hostname only, do not begin with http://" + embeddable_hosts: "Host(s) that can embed the comments from this Discourse forum. Hostname only, do not begin with http://" feed_polling_enabled: "EMBEDDING ONLY: Whether to embed a RSS/ATOM feed as posts." feed_polling_url: "EMBEDDING ONLY: URL of RSS/ATOM feed to embed." embed_by_username: "Discourse username of the user who creates the embedded topics." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 707c6b407b..d9808f08f7 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -959,7 +959,7 @@ es: warn_reviving_old_topic_age: "Cuando alguien publica en un tema cuya última respuesta fue hace este número de días o más, se le mostrará un aviso para desalentar el hecho de resucitar una antigua discusión. Deshabilita esta opción introduciendo el valor 0." autohighlight_all_code: "Forzar el resaltado de código a los bloques de código preformateado cuando no se especifique el lenguaje del código." highlighted_languages: "Incluye reglas resaltadas de sintaxis. (Advertencia: incluyendo demasiadas lenguages puede afectar al rendimiento) ver: https://highlightjs.org/static/demo/ para una demostración" - embeddable_host: "Host que puede incrustar los comentarios de este foro Discourse. Nombre de host solamente, no comienzan con http://" + embeddable_hosts: "Host que puede incrustar los comentarios de este foro Discourse. Nombre de host solamente, no comienzan con http://" feed_polling_enabled: "SOLO PARA EMBEBER: embeber feeds RSS/ATOM como posts." feed_polling_url: "SOLO PARA EMBEBER: URL de los feeds RSS/ATOM a embeber." embed_by_username: "Nombre de usuario en Discourse del que crea los temas embebidos." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 7b80c9f829..f1b1be2b99 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -916,7 +916,7 @@ fa_IR: warn_reviving_old_topic_age: "وقتی کسی شروع می کند با پاسخ دادن به جستاری که آخرین پاسخ برمی گردد به خیلی قبل یک هشدار نمایش داده می شود. نمایش با تنظیمات تا 0. " autohighlight_all_code: "اعمال زور برای برجسته کردن کد به تمام بلاک های کد تنظیم نشده حتی وقتی به صراحت زبان را مشخص نمی کنند. " highlighted_languages: "شامل نحوه قوانین برجسته شده. ( اخطار: شامل زبان های متفاوت ممکن است در نحوه اجرا تاثیر گذار باشد) ببین: https://highlightjs.org/static/demo/ برای دمو" - embeddable_host: "سروری که می تونه در نوشته ها جا سازی بشه از انجمن دیسکورس. فقط نام سروری که با http:// شروع می شود" + embeddable_hosts: "سروری که می تونه در نوشته ها جا سازی بشه از انجمن دیسکورس. فقط نام سروری که با http:// شروع می شود" feed_polling_enabled: "فقط جاسازی: چه جاسازی RSS/ATOM feed به عنوان نوشته" feed_polling_url: "فقط جاسازی: URL of RSS/ATOM feed to embed." embed_by_username: "نام کاربری دیسکورس کاربری که نوشته های جاسازی شده را ساخته." diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 2d73da7ff6..be1af1cbc8 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -962,7 +962,7 @@ fi: warn_reviving_old_topic_age: "Kun käyttäjä alkaa kirjoittamaan vastausta ketjuun, jonka uusin viesti on tätä vanhempi päivissä, näytetään varoitus. Poista käytöstä asettamalla arvoksi 0." autohighlight_all_code: "Pakota koodin korostus kaikkiin esimuotoiltuihin tekstiblokkeihin, vaikka käyttäjä ei määrittelisi kieltä." highlighted_languages: "Syntaksin korostamisen säännöt. (Varoitus: liian monen kielen sisällyttäminen voi vaikuttaa suorituskykyyn) katso demo: https://highlightjs.org/static/demo/" - embeddable_host: "Isäntä, joka voi upottaa kommentteja tältä palstalta. Pelkkä isäntänimi, älä aloita http://" + embeddable_hosts: "Isäntä, joka voi upottaa kommentteja tältä palstalta. Pelkkä isäntänimi, älä aloita http://" feed_polling_enabled: "VAIN UPOTUS: Upotetaanko RSS/ATOM syöte viesteinä." feed_polling_url: "VAIN UPOTUS: RSS/ATOM syötteen URL." embed_by_username: "Sen käyttäjän Discourse käyttäjänimi, joka luo upotetut ketjut." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 6eba5d754d..380ff34846 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -963,7 +963,7 @@ fr: warn_reviving_old_topic_age: "Lorsque quelqu'un commence à répondre à un sujet dont la dernière réponse est vielle de plusieurs jours, un avertissement sera affiché. Désactiver la fonctionnalité en indiquant: 0." autohighlight_all_code: "Forcer la mise en évidence de tout les textes dans les balises code, même si ils ne correspondent à aucun langage de programmation." highlighted_languages: "Include les règles de mise en surbrillance de syntaxe. (Avertissement: l'ajout de trop de langages peut impacter les performances) voir l'exemple https://highlightjs.org/static/demo/" - embeddable_host: "Hôte qui peut incorporer des messages de ce forum Discourse. Nom d'hôte seulement, sans http://" + embeddable_hosts: "Hôte qui peut incorporer des messages de ce forum Discourse. Nom d'hôte seulement, sans http://" feed_polling_enabled: "EMBARQUER UNIQUEMENT: Embarqué le flux RSS/ATOM en tant que messages." feed_polling_url: "EMBARQUER UNIQUEMENT: Url du flux RSS/ATOM à embarqué." embed_by_username: "Pseudo de l'utilisateur Discourse qui crée les sujets embarqués." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 4b89ec7e52..b16af98532 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -925,7 +925,7 @@ he: warn_reviving_old_topic_age: "כאשר מישהו/מישהי מתחילים להגיב לנושא שבו התגובה האחרונה היא בת יותר מכמה ימים, אזהרה תוצג. בטלו אפשרות זו באמצעות הזנה של 0." autohighlight_all_code: "לחייב שימוש בקוד הדגשה לכל קוד מעוצב מראש (preformatted code blocks) אפילו אם הם אינם מציינים את השפה." highlighted_languages: "הכללת הדגשת שגיאות תחביר. (אזהרה: הכללת שפות רבות מידי עשוי להשפיע על הביצוע) ראו: https://highlightjs.org/static/demo/ להדגמה" - embeddable_host: "מארח (Host) אשר יכול להטמיע את התגובות מפורום Discourse זה. שם מארח בלבד, ללא http:// בהתחלה" + embeddable_hosts: "מארח (Host) אשר יכול להטמיע את התגובות מפורום Discourse זה. שם מארח בלבד, ללא http:// בהתחלה" feed_polling_enabled: "הטמעה בלבד: האם לעמבד פידים של RSS/ATOM כפרסומים." feed_polling_url: "הטמעה בלבד: URL של פיד RSS/ATOM להטמעה." embed_by_username: "שם המשתמש של המשתמש/ת שיוצר את הנושאים המוטמעים." diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 2a20f5ba6d..3544584d66 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -837,7 +837,7 @@ ja: warn_reviving_old_topic_age: "最後の返信がこの設定よりも古いトピックに返信すると、警告を表示します。0を設定すると無効になります" autohighlight_all_code: "明示的に言語を指定しなくても、全てのコードブロックにコードハイライトを強制的に適用する" highlighted_languages: "適用するシンタックスハイライトルール(警告: あまりに多くの言語を含むとパフォーマンスに影響を与えます) デモ: https://highlightjs.org/static/demo/ " - embeddable_host: "このDiscourseフォーラムのコメントを埋め込む事が出来るHost。Host名のみ、http:// で始めない" + embeddable_hosts: "このDiscourseフォーラムのコメントを埋め込む事が出来るHost。Host名のみ、http:// で始めない" feed_polling_enabled: "EMBEDDING ONLY: ポストとしてRSS/Atomフィードを埋め込むかどうか" feed_polling_url: "EMBEDDING ONLY: RSS/ATOMフィードのURLを埋め込む事が出来ます" embed_by_username: "embedされたトピックの作成者として表示されるDiscourseユーザー名" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 2dc6087fb2..1bb604cc61 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -950,7 +950,7 @@ pt: warn_reviving_old_topic_age: "Quando alguém começa a responder a um tópico em que a última resposta é mais antiga que estes dias, um aviso será exibido. Desativar ao configurar para 0." autohighlight_all_code: "Forçar o destaque do código a todos os blocos de código pré-formatados mesmo quando não se especifica a linguagem." highlighted_languages: "Incluídas regras de destaque da sintaxe (Aviso: incluir demasiadas linguagens pode impactar o desempenho) ver: https://highlightjs.org/static/demo/ para uma demonstração" - embeddable_host: "Servidor que pode incorporar os comentários deste fórum Discourse. Apenas o nome do servidor, não começar com http://" + embeddable_hosts: "Servidor que pode incorporar os comentários deste fórum Discourse. Apenas o nome do servidor, não começar com http://" feed_polling_enabled: "INCORPORAR APENAS: incorporar feeds RSS/ATOM como mensagens." feed_polling_url: "INCORPORAR APENAS: URL dos feeds de RSS/ATOM para embutir." embed_by_username: "Nome de utilizador Discourse do utilizador que cria tópicos embebidos." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 9e6ed66873..07e62e329d 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -950,7 +950,7 @@ pt_BR: warn_reviving_old_topic_age: "Quando alguém começa a responder a um tópico mais velho do que este número de dias, um aviso será exibido para desencorajar o usuário de reviver uma velha discussão. Desabilite definindo para 0." autohighlight_all_code: "Aplicar código destacando todos os blocos de código pré-formatados, mesmo quando não for específica o idioma" highlighted_languages: "Incluir regras de destaque de sintaxe. (AVISO: incluir muitas linguagens podem afetar a performance) veja: https://highlightjs.org/static/demo/ para uma demonstração" - embeddable_host: "Servidor que pode incorporar os comentários deste forum Discourse. Apenas o Nome do Servidor, não começar com http://" + embeddable_hosts: "Servidor que pode incorporar os comentários deste forum Discourse. Apenas o Nome do Servidor, não começar com http://" feed_polling_enabled: "Se um feed RSS / ATOM são importados como mensagens" feed_polling_url: "URL do feed RSS / ATOM para importar" embed_by_username: "Nome de usuário Discourse para o usuário que cria os tópicos" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 2f988dea9e..f0a862aa69 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -1005,7 +1005,7 @@ ru: warn_reviving_old_topic_age: "Показывать предупреждение, когда кто-то пытается ответить в очень старую тему. Указывается в днях. Чтобы отключить 0." autohighlight_all_code: "Принудительно использовать подсветку кода для всех отформатированных блоков кода, даже когда они явно не указан язык." highlighted_languages: "Включить правила подсветки синтаксиса. (ВНИМАНИЕ: включение для многих языков может вызвать проблемы с производительностью) пример можно посмотреть на: https://highlightjs.org/static/demo/ " - embeddable_host: "Имя хоста которому разрешено использовать комменты с данного форума. Не указывайте http://" + embeddable_hosts: "Имя хоста которому разрешено использовать комменты с данного форума. Не указывайте http://" feed_polling_enabled: "ТОЛЬКО ДЛЯ ВЛОЖЕННЫХ: Встраивать ли вложенные сообщения в RSS/ATOM ленту" feed_polling_url: "URL адрес импорта RSS/ATOM ленты" embed_by_username: "Имя пользователя который созал вложенную тему" diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index ed59ff9198..c692d86810 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -938,7 +938,7 @@ sq: warn_reviving_old_topic_age: "When someone starts replying to a topic where the last reply is older than this many days, a warning will be displayed. Disable by setting to 0." autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language." highlighted_languages: "Included syntax highlighting rules. (Warning: including too many langauges may impact performance) see: https://highlightjs.org/static/demo/ for a demo" - embeddable_host: "Host that can embed the comments from this Discourse forum. Hostname only, do not begin with http://" + embeddable_hosts: "Host that can embed the comments from this Discourse forum. Hostname only, do not begin with http://" feed_polling_enabled: "EMBEDDING ONLY: Whether to embed a RSS/ATOM feed as posts." feed_polling_url: "EMBEDDING ONLY: URL of RSS/ATOM feed to embed." embed_by_username: "Discourse username of the user who creates the embedded topics." diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index eea2bfc804..ee726c2a3d 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -903,7 +903,7 @@ tr_TR: warn_reviving_old_topic_age: "Herhangi bir kullanıcı, son cevabın burada belirtilen gün sayısından daha önce yazıldığı bir konuya cevap yazmaya başladığında, bir uyarı mesajı çıkacak. Bu özelliği devre dışı bırakmak için 0 girin. " autohighlight_all_code: "Tüm önceden formatlanan kod bloklarına, açıkça dil seçimi yapılmamış olsa da, zorla kod vurgulaması uygula." highlighted_languages: "Dahil edilen sözdizimi vurgulama kuralları. (Dikkat: çok fazla dili dahil etmek performansı etkileyebilir) Demo için https://highlightjs.org/static/demo/ adresine bakınız" - embeddable_host: "Bu Discourse forumundan yorumların yerleştirilebileceği sunucu. Sadece sunucu, http:// ile başlamayın" + embeddable_hosts: "Bu Discourse forumundan yorumların yerleştirilebileceği sunucu. Sadece sunucu, http:// ile başlamayın" feed_polling_enabled: "SADECE YERLEŞTİRME İÇİN: RSS/ATOM beslemesinin gönderi olarak yerleştirilip yerleştirilemeyeceği." feed_polling_url: "SADECE YERLEŞTİRME İÇİN: Yerleştirilecek RSS/ATOM beslemesinin URL'i." embed_by_username: "Yerleştirilmiş konuları oluşturan kullanıcıya ait Discourse kullanıcı adı. " diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 0af033048a..6bb6b7ded3 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -934,7 +934,7 @@ zh_CN: warn_reviving_old_topic_age: "当有人开始回复最后一贴超过一定天数前的主题时,将有一个警告显示,不鼓励他们复活一个老的讨论。将其设置为 0 以禁用。" autohighlight_all_code: "即使未显式设定语言,仍为所有预编排代码块应用语法高亮。" highlighted_languages: "包含语法高亮规则。(警告:包含太多的语言可能会印象性能)见:https://highlightjs.org/static/demo/ 查看演示" - embeddable_host: "能从这个 Discourse 论坛嵌入评论的主机。\n仅主机名,不要以 http:// 开头" + embeddable_hosts: "能从这个 Discourse 论坛嵌入评论的主机。\n仅主机名,不要以 http:// 开头" feed_polling_enabled: "仅用于嵌入:是否将 RSS/ATOM 订阅为帖子。" feed_polling_url: "仅用于嵌入:RSS/ATOM 订阅的 URL。" embed_by_username: "创建嵌入主题的 Discourse 的用户名。" diff --git a/config/site_settings.yml b/config/site_settings.yml index c514c80297..8f2d141fef 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -729,9 +729,9 @@ developer: client: true embedding: - embeddable_host: - default: '' - regex: "^(?!http).+" # don't allow this to start with http:// or https:// + embeddable_hosts: + default: '' + type: host_list feed_polling_enabled: false feed_polling_url: '' embed_by_username: diff --git a/db/migrate/20150609163211_migrate_embeddable_host.rb b/db/migrate/20150609163211_migrate_embeddable_host.rb new file mode 100644 index 0000000000..d3d351a32b --- /dev/null +++ b/db/migrate/20150609163211_migrate_embeddable_host.rb @@ -0,0 +1,5 @@ +class MigrateEmbeddableHost < ActiveRecord::Migration + def change + execute "UPDATE site_settings SET name = 'embeddable_hosts', data_type = 9 WHERE name = 'embeddable_host'" + end +end diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 456f94d336..facb22bb4b 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -14,7 +14,7 @@ module SiteSettingExtension end def types - @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum, :list, :url_list) + @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum, :list, :url_list, :host_list) end def mutex diff --git a/lib/topic_retriever.rb b/lib/topic_retriever.rb index 161f362be4..1682377309 100644 --- a/lib/topic_retriever.rb +++ b/lib/topic_retriever.rb @@ -13,10 +13,7 @@ class TopicRetriever private def invalid_host? - SiteSetting.normalized_embeddable_host != URI(@embed_url).host - rescue URI::InvalidURIError - # An invalid URI is an invalid host - true + SiteSetting.allows_embeddable_host?(@embed_url) end def retrieved_recently? diff --git a/spec/controllers/embed_controller_spec.rb b/spec/controllers/embed_controller_spec.rb index e68965d9bd..514d84f57c 100644 --- a/spec/controllers/embed_controller_spec.rb +++ b/spec/controllers/embed_controller_spec.rb @@ -11,14 +11,14 @@ describe EmbedController do end it "raises an error with a missing host" do - SiteSetting.stubs(:embeddable_host).returns(nil) + SiteSetting.embeddable_hosts = nil get :comments, embed_url: embed_url expect(response).not_to be_success end context "with a host" do before do - SiteSetting.stubs(:embeddable_host).returns(host) + SiteSetting.embeddable_hosts = host end it "raises an error with no referer" do @@ -27,7 +27,6 @@ describe EmbedController do end context "success" do - before do controller.request.stubs(:referer).returns(embed_url) end @@ -51,8 +50,31 @@ describe EmbedController do get :comments, embed_url: embed_url end end - end + context "with multiple hosts" do + before do + SiteSetting.embeddable_hosts = "#{host}\nhttp://discourse.org" + end + context "success" do + it "works with the first host" do + controller.request.stubs(:referer).returns("http://eviltrout.com/wat/1-2-3.html") + get :comments, embed_url: embed_url + expect(response).to be_success + end + + it "works with the second host" do + controller.request.stubs(:referer).returns("https://discourse.org/blog-entry-1") + get :comments, embed_url: embed_url + expect(response).to be_success + end + + it "doesn't work with a made up host" do + controller.request.stubs(:referer).returns("http://codinghorror.com/invalid-url") + get :comments, embed_url: embed_url + expect(response).to_not be_success + end + end + end end diff --git a/spec/models/site_setting_spec.rb b/spec/models/site_setting_spec.rb index 0cd8691a66..a4ca870d5d 100644 --- a/spec/models/site_setting_spec.rb +++ b/spec/models/site_setting_spec.rb @@ -4,21 +4,34 @@ require_dependency 'site_setting_extension' describe SiteSetting do - describe "normalized_embeddable_host" do - it 'returns the `embeddable_host` value' do - SiteSetting.stubs(:embeddable_host).returns("eviltrout.com") - expect(SiteSetting.normalized_embeddable_host).to eq("eviltrout.com") + describe "allows_embeddable_host" do + it 'works as expected' do + SiteSetting.embeddable_hosts = 'eviltrout.com' + expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) + expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true) + expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false) end - it 'strip http from `embeddable_host` value' do - SiteSetting.stubs(:embeddable_host).returns("http://eviltrout.com") - expect(SiteSetting.normalized_embeddable_host).to eq("eviltrout.com") + it 'works with a http host' do + SiteSetting.embeddable_hosts = 'http://eviltrout.com' + expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) + expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true) + expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false) end - it 'strip https from `embeddable_host` value' do - SiteSetting.stubs(:embeddable_host).returns("https://eviltrout.com") - expect(SiteSetting.normalized_embeddable_host).to eq("eviltrout.com") + it 'works with a https host' do + SiteSetting.embeddable_hosts = 'https://eviltrout.com' + expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) + expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true) + expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false) end + + it 'works with multiple hosts' do + SiteSetting.embeddable_hosts = "https://eviltrout.com\nhttps://discourse.org" + expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) + expect(SiteSetting.allows_embeddable_host?('http://discourse.org')).to eq(true) + end + end describe 'topic_title_length' do diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 673c7e9b69..ff971fa737 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1400,8 +1400,8 @@ describe Topic do let(:topic) { Fabricate.build(:topic) } before do - SiteSetting.stubs(:embeddable_host).returns("http://eviltrout.com") - SiteSetting.stubs(:embed_truncate?).returns(true) + SiteSetting.embeddable_hosts = "http://eviltrout.com" + SiteSetting.embed_truncate = true topic.stubs(:has_topic_embed?).returns(true) end @@ -1410,12 +1410,12 @@ describe Topic do end it "is false if embeddable_host is blank" do - SiteSetting.stubs(:embeddable_host).returns(nil) + SiteSetting.embeddable_hosts = nil expect(topic.expandable_first_post?).to eq(false) end it "is false if embed_truncate? is false" do - SiteSetting.stubs(:embed_truncate?).returns(false) + SiteSetting.embed_truncate = false expect(topic.expandable_first_post?).to eq(false) end diff --git a/test/javascripts/components/value-list-test.js.es6 b/test/javascripts/components/value-list-test.js.es6 new file mode 100644 index 0000000000..db8767471b --- /dev/null +++ b/test/javascripts/components/value-list-test.js.es6 @@ -0,0 +1,31 @@ +moduleForComponent('value-list', {integration: true}); + +test('functionality', function(assert) { + andThen(() => { + this.render('{{value-list value=values}}'); + }); + + andThen(() => { + 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'); + andThen(() => { + assert.ok(!this.$('.btn-primary[disabled]').length, "it isn't disabled anymore"); + }); + + click('.btn-primary'); + andThen(() => { + assert.ok(this.$('.values .value').length === 1, 'it adds the value'); + assert.ok(this.$('input').val() === '', 'it clears the input'); + assert.ok(this.$('.btn-primary[disabled]').length, "it is disabled again"); + }); + + click('.value .btn-small'); + andThen(() => { + assert.ok(this.$('.values .value').length === 0, 'it removes the value'); + }); + +}); From 90eca69e0a0574e458d6960e90b0eae083e431d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 9 Jun 2015 19:31:14 +0200 Subject: [PATCH 25/60] FEATURE: add a link to the topic in the banner for staff members --- .../discourse/templates/components/discourse-banner.hbs | 3 +++ app/models/topic.rb | 3 ++- config/locales/client.en.yml | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs b/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs index 39b419d6ff..1a647c5ea8 100644 --- a/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs +++ b/app/assets/javascripts/discourse/templates/components/discourse-banner.hbs @@ -3,6 +3,9 @@ diff --git a/app/models/topic.rb b/app/models/topic.rb index d9ebbd3af1..6f444e1ab4 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -662,7 +662,8 @@ class Topic < ActiveRecord::Base { html: post.cooked, - key: self.id + key: self.id, + url: self.url } end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f4a746ba60..8e1e1b142e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -218,6 +218,7 @@ en: banner: close: "Dismiss this banner." + edit: "Edit this banner >>" choose_topic: none_found: "No topics found." From 9049f31456186b34b099fce9829684af704402d9 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 10 Jun 2015 00:52:11 +0530 Subject: [PATCH 26/60] add posts.rss rel alternate --- app/views/list/list.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/list/list.erb b/app/views/list/list.erb index 7382cea1c0..dd75051b40 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -41,6 +41,7 @@ <% if @rss %> <% content_for :head do %> + <%= auto_discovery_link_tag(:rss, "#{Discourse.base_url}/posts.rss", title: I18n.t("rss_description.posts")) %> <%= auto_discovery_link_tag(:rss, { action: "#{@rss}_feed" }, title: I18n.t("rss_description.#{@rss}")) %> <% end %> <% end %> From 79027c2775ede0ae2377c86a8bc2d6c282db3244 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 10 Jun 2015 06:05:28 +1000 Subject: [PATCH 27/60] EXTENSIBILITY: add category-custom-settings outlet --- .../discourse/templates/modal/edit-category-settings.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/discourse/templates/modal/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/modal/edit-category-settings.hbs index 2b8479e2bd..b6b08f881f 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-category-settings.hbs @@ -43,3 +43,5 @@ {{i18n 'category.position_disabled_click'}} {{/if}} + +{{plugin-outlet "category-custom-settings"}} From e3fa27a01cd591ea202ff805d79c2b6db6505d22 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 10 Jun 2015 06:07:37 +1000 Subject: [PATCH 28/60] FEATURE: serialize and update category custom_fields - send to client - update from client --- app/assets/javascripts/discourse/models/category.js | 3 ++- app/controllers/categories_controller.rb | 1 + app/serializers/category_serializer.rb | 3 ++- spec/controllers/categories_controller_spec.rb | 5 +++++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index c354f0d48b..4f8ede69e7 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -75,7 +75,8 @@ Discourse.Category = Discourse.Model.extend({ parent_category_id: this.get('parent_category_id'), logo_url: this.get('logo_url'), background_url: this.get('background_url'), - allow_badges: this.get('allow_badges') + allow_badges: this.get('allow_badges'), + custom_fields: this.get('custom_fields') }, type: this.get('id') ? 'PUT' : 'POST' }); diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 8199472107..c00b67876d 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -146,6 +146,7 @@ class CategoriesController < ApplicationController :background_url, :allow_badges, :slug, + :custom_fields => [params[:custom_fields].try(:keys)], :permissions => [*p.try(:keys)]) end end diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index 8242a57ff7..1757652094 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -10,7 +10,8 @@ class CategorySerializer < BasicCategorySerializer :email_in_allow_strangers, :can_delete, :cannot_delete_reason, - :allow_badges + :allow_badges, + :custom_fields def group_permissions @group_permissions ||= begin diff --git a/spec/controllers/categories_controller_spec.rb b/spec/controllers/categories_controller_spec.rb index 3aa0d31e76..e838b2e971 100644 --- a/spec/controllers/categories_controller_spec.rb +++ b/spec/controllers/categories_controller_spec.rb @@ -162,8 +162,12 @@ describe CategoriesController do 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([ @@ -173,6 +177,7 @@ describe CategoriesController do 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 end end From 49ca2481866906b38ce093a436b8085669f85855 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 10 Jun 2015 06:08:06 +1000 Subject: [PATCH 29/60] FEATURE: allow distributed cache to handle Set as value --- lib/distributed_cache.rb | 7 ++++-- spec/components/distributed_cache_spec.rb | 28 +++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/lib/distributed_cache.rb b/lib/distributed_cache.rb index 777c9b780e..58974b77ad 100644 --- a/lib/distributed_cache.rb +++ b/lib/distributed_cache.rb @@ -31,7 +31,7 @@ class DistributedCache hash = current.hash(message.site_id) case payload["op"] - when "set" then hash[payload["key"]] = payload["value"] + when "set" then hash[payload["key"]] = payload["marshalled"] ? Marshal.load(payload["value"]) : payload["value"] when "delete" then hash.delete(payload["key"]) when "clear" then hash.clear end @@ -69,7 +69,10 @@ class DistributedCache end def self.set(hash, key, value) - publish(hash, { op: :set, key: key, value: value }) + # special support for set + marshal = Set === value + value = Marshal.dump(value) if marshal + publish(hash, { op: :set, key: key, value: value, marshalled: marshal }) end def self.delete(hash, key) diff --git a/spec/components/distributed_cache_spec.rb b/spec/components/distributed_cache_spec.rb index 68a0c259a4..a26f6fbbd3 100644 --- a/spec/components/distributed_cache_spec.rb +++ b/spec/components/distributed_cache_spec.rb @@ -11,6 +11,34 @@ describe DistributedCache do DistributedCache.new("test") end + it 'allows us to store Set' do + c1 = DistributedCache.new("test1") + c2 = DistributedCache.new("test1") + + set = {a: 1, b: 1} + set = Set.new + set << 1 + set << "b" + + c1["cats"] = set + + wait_for do + c2["cats"] == set + end + + expect(c2["cats"]).to eq(set) + + set << 5 + + c2["cats"] == set + + wait_for do + c1["cats"] == set + end + + expect(c1["cats"]).to eq(set) + end + it 'does not leak state across caches' do c2 = DistributedCache.new("test1") c3 = DistributedCache.new("test1") From ae277e28a64840433141f8085ab9a080ebce5ef2 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 9 Jun 2015 16:24:04 -0400 Subject: [PATCH 30/60] FEATURE: Allow embedding topics without creating them, by id --- app/controllers/embed_controller.rb | 15 ++++-- public/javascripts/embed.js | 57 +++++++++++++++-------- spec/controllers/embed_controller_spec.rb | 14 ++++++ 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index 198288297a..f296998ef0 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -1,13 +1,18 @@ class EmbedController < ApplicationController skip_before_filter :check_xhr, :preload_json, :verify_authenticity_token - before_filter :ensure_embeddable layout 'embed' def comments - embed_url = params.require(:embed_url) - topic_id = TopicEmbed.topic_id_for_embed(embed_url) + embed_url = params[:embed_url] + + topic_id = nil + if embed_url.present? + topic_id = TopicEmbed.topic_id_for_embed(embed_url) + else + topic_id = params[:topic_id].to_i + end if topic_id @topic_view = TopicView.new(topic_id, @@ -21,7 +26,8 @@ class EmbedController < ApplicationController if @topic_view && @topic_view.posts.size == SiteSetting.embed_post_limit @posts_left = @topic_view.topic.posts_count - SiteSetting.embed_post_limit - 1 end - else + + elsif embed_url.present? Jobs.enqueue(:retrieve_topic, user_id: current_user.try(:id), embed_url: embed_url) render 'loading' end @@ -30,7 +36,6 @@ class EmbedController < ApplicationController end def count - embed_urls = params[:embed_url] by_url = {} diff --git a/public/javascripts/embed.js b/public/javascripts/embed.js index e299ae5476..33c11d337a 100644 --- a/public/javascripts/embed.js +++ b/public/javascripts/embed.js @@ -1,24 +1,41 @@ -/* global discourseUrl */ -/* global discourseUserName */ -/* global discourseEmbedUrl */ (function() { - var comments = document.getElementById('discourse-comments'), - iframe = document.createElement('iframe'); - if (typeof discourseUserName === 'undefined') { - iframe.src = - [ discourseUrl, - 'embed/comments?embed_url=', - encodeURIComponent(discourseEmbedUrl) - ].join(''); - } else { - iframe.src = - [ discourseUrl, - 'embed/comments?embed_url=', - encodeURIComponent(discourseEmbedUrl), - '&discourse_username=', - discourseUserName - ].join(''); + + var DE = window.DiscourseEmbed || {}; + var comments = document.getElementById('discourse-comments'); + var iframe = document.createElement('iframe'); + + ['discourseUrl', 'discourseEmbedUrl', 'discourseUserName'].forEach(function(i) { + if (window[i]) { DE[i] = DE[i] || window[i]; } + }); + + var queryParams = {}; + + if (DE.discourseEmbedUrl) { + queryParams.embed_url = encodeURIComponent(DE.discourseEmbedUrl); } + + if (DE.discourseUserName) { + queryParams.discourse_username = DE.discourseUserName; + } + + if (DE.topicId) { + queryParams.topic_id = DE.topicId; + } + + var src = DE.discourseUrl + 'embed/comments'; + var keys = Object.keys(queryParams); + if (keys.length > 0) { + src += "?"; + + for (var i=0; i 0) { src += "&"; } + + var k = keys[i]; + src += k + "=" + queryParams[k]; + } + } + + iframe.src = src; iframe.id = 'discourse-embed-frame'; iframe.width = "100%"; iframe.frameBorder = "0"; @@ -48,7 +65,7 @@ function postMessageReceived(e) { if (!e) { return; } - if (discourseUrl.indexOf(e.origin) === -1) { return; } + if (DE.discourseUrl.indexOf(e.origin) === -1) { return; } if (e.data) { if (e.data.type === 'discourse-resize' && e.data.height) { diff --git a/spec/controllers/embed_controller_spec.rb b/spec/controllers/embed_controller_spec.rb index 514d84f57c..4fc2319d62 100644 --- a/spec/controllers/embed_controller_spec.rb +++ b/spec/controllers/embed_controller_spec.rb @@ -16,6 +16,20 @@ describe EmbedController do expect(response).not_to be_success end + context "by topic id" do + + before do + SiteSetting.embeddable_hosts = host + controller.request.stubs(:referer).returns('http://eviltrout.com/some-page') + end + + it "allows a topic to be embedded by id" do + topic = Fabricate(:topic) + get :comments, topic_id: topic.id + expect(response).to be_success + end + end + context "with a host" do before do SiteSetting.embeddable_hosts = host From 195cdaec11b143d3d9e98cc2f813038359fd978d Mon Sep 17 00:00:00 2001 From: dfabulich Date: Tue, 9 Jun 2015 13:34:20 -0700 Subject: [PATCH 31/60] Convert author tag to dc:creator RSS spec says the author tag should be an email address, forcing us to put in a junk no-reply@example.com email. Instead, we should use dc:creator, which allows us to use any name we want for the user. --- app/views/list/list.rss.erb | 4 ++-- app/views/posts/latest.rss.erb | 4 ++-- app/views/topics/show.rss.erb | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/views/list/list.rss.erb b/app/views/list/list.rss.erb index deb5e80f02..b1eb41f103 100644 --- a/app/views/list/list.rss.erb +++ b/app/views/list/list.rss.erb @@ -1,5 +1,5 @@ - + <% lang = SiteSetting.find_by_name('default_locale').try(:value) %> <% site_email = SiteSetting.find_by_name('contact_email').try(:value) %> @@ -14,7 +14,7 @@ <% topic_url = topic.url -%> <%= topic.title %> - <%= "no-reply@example.com (@#{topic.user.username}#{" #{topic.user.name}" if (topic.user.name.present? && SiteSetting.enable_names?)})" -%> + ]]> <%= topic.category.name %> <%= t('author_wrote', author: link_to("@#{topic.user.username}", "#{Discourse.base_url}/users/#{topic.user.username_lower}")).html_safe %>

diff --git a/app/views/posts/latest.rss.erb b/app/views/posts/latest.rss.erb index 04aeb02787..7cfb0a8e1d 100644 --- a/app/views/posts/latest.rss.erb +++ b/app/views/posts/latest.rss.erb @@ -1,5 +1,5 @@ - + <% lang = SiteSetting.find_by_name('default_locale').try(:value) %> <% site_email = SiteSetting.find_by_name('contact_email').try(:value) %> @@ -10,7 +10,7 @@ <% next unless post.user %> <%= post.topic.title %> - <%= "no-reply@example.com (@#{post.user.username}#{" #{post.user.name}" if (post.user.name.present? && SiteSetting.enable_names?)})" -%> + ]]> ]]> <%= Discourse.base_url + post.url %> <%= post.created_at.rfc2822 %> diff --git a/app/views/topics/show.rss.erb b/app/views/topics/show.rss.erb index 03b5471ecb..e49890e2d0 100644 --- a/app/views/topics/show.rss.erb +++ b/app/views/topics/show.rss.erb @@ -1,5 +1,5 @@ - + <% topic_url = @topic_view.absolute_url %> <% lang = SiteSetting.find_by_name('default_locale').try(:value) %> @@ -15,7 +15,7 @@ <% next unless post.user %> <%= @topic_view.title %> - <%= "no-reply@example.com (@#{post.user.username}#{" #{post.user.name}" if (post.user.name.present? && SiteSetting.enable_names?)})" -%> + ]]>

<%= t('author_wrote', author: link_to("@#{post.user.username}", user_url(post.user.username_lower))).html_safe %>

From d127e1179f39b977d6156b471208113170b19c12 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 10 Jun 2015 14:39:29 +0800 Subject: [PATCH 32/60] FIX: Incorrect check when no text is selected. --- .../javascripts/discourse/controllers/quote-button.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 index 0a4c0e3ada..c99f57247d 100644 --- a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 @@ -27,7 +27,7 @@ export default DiscourseController.extend({ const selection = window.getSelection(); // no selections - if (selection.rangeCount === 0) return; + if (selection.isCollapsed) return; // retrieve the selected range const range = selection.getRangeAt(0), From bf8c9c34110fdea5724067c4abc22e21b9f0235e Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Wed, 10 Jun 2015 18:30:29 +1000 Subject: [PATCH 33/60] FEATURE: ship user_id with topic serializer --- app/serializers/topic_view_serializer.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index c2d98f077b..1fd56f54a1 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -32,7 +32,8 @@ class TopicViewSerializer < ApplicationSerializer :category_id, :word_count, :deleted_at, - :pending_posts_count + :pending_posts_count, + :user_id attributes :draft, :draft_key, From c5b6ace07bd1c5195c2e34f2fda1ebcabff7abb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Jun 2015 10:32:02 +0200 Subject: [PATCH 34/60] update onebox to latest --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e7b6d30551..e50f2484f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -208,7 +208,7 @@ GEM omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) - onebox (1.5.19) + onebox (1.5.20) moneta (~> 0.7) multi_json (~> 1.7) mustache (~> 0.99) From effe83d7a961b2b0334d9aec1905559c8015c01b Mon Sep 17 00:00:00 2001 From: Noam Yorav-Raphael Date: Wed, 10 Jun 2015 11:47:07 +0300 Subject: [PATCH 35/60] Don't limit @mention autocomplete to latin characters The userSearch() function, used for @mention autocomplete, returned an empty list if the query string included non-latin characters or spaces. This removes this restriction, so you can search users by any characters in their display name, including spaces. --- app/assets/javascripts/discourse/lib/user-search.js.es6 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 790a00ff09..068282c75b 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -88,11 +88,6 @@ export default function userSearch(options) { currentTerm = term; return new Ember.RSVP.Promise(function(resolve) { - // TODO site setting for allowed regex in username - if (term.match(/[^a-zA-Z0-9_\.]/)) { - resolve([]); - return; - } if (((new Date() - cacheTime) > 30000) || (cacheTopicId !== topicId)) { cache = {}; } From 677cdbbda0f83dbbd5ea4dfbfaff3895168efc07 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Jun 2015 02:26:25 -0700 Subject: [PATCH 36/60] updated welcome usage tips and images --- config/locales/server.en.yml | 42 +++++++++--------- public/images/welcome/notification-panel.png | Bin 3885 -> 3729 bytes public/images/welcome/progress-bar.png | Bin 1528 -> 1083 bytes public/images/welcome/username-completion.png | Bin 15028 -> 15521 bytes 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index bb6ce41067..6ce907efab 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1458,43 +1458,43 @@ en: usage_tips: text_body_template: | - A few quick tips to get you started: + Here are a few quick tips to get you started: - ## Keep scrolling + ## Just Scroll Down - There are no next page buttons or page numbers – to read more, **just keep scrolling down!** + To read more, **just keep scrolling down!** As new posts or new topics arrive, they will appear automatically. - As new posts come in, they will appear automatically. + ## Navigation - ## Where am I? + - For search, your user page, or the menu, use the **icon buttons at upper right**. - - For search, your user page, or the menu, use the **icon buttons at the upper right**. + - Selecting a topic title will always take you to the next unread post in the topic. To enter at the top or bottom instead, select the date or post count. - - Any topic title will take you to the next unread post. Use the last activity time and post count to enter at the top or bottom. - - - While reading a topic, jump to the top ↑ by selecting the topic title. Select the green progress bar at the bottom right for full navigation controls, or use the home and end keys. + - While reading a topic, jump to the top ↑ by selecting the topic title. Select the progress bar at the bottom right for full navigation controls, or use the home and end keys. - ## How do I reply? + ## Replying - - To reply to the overall topic, use the Reply button at the very bottom of the page. + To reply … - - To reply to a specific post, use the Reply button on that post. + - to the **topic in general**, use the Reply button at the very bottom of the topic. - - To take the conversation in a different direction, but keep them linked together, use Reply as linked Topic to the right of the post. + - to a **specific person**, use the Reply button on their post. - To quote someone in your reply, select the text you wish to quote, then press any Reply button. + - with **a linked topic**, use Reply as linked Topic to the right of the post. + + To quote, simply select the text you wish to quote, then press any Reply button. - To ping someone in your reply, mention their name. Type `@` and an autocompleter will pop up. + To ping someone in your reply, mention their name. Type `@` to begin selecting a name. - For [standard Emoji](http://www.emoji.codes/), just start typing `:` or the traditional smileys `:)` :smile: + For [standard Emoji](http://www.emoji.codes/), just type `:` or the traditional smileys `:)` :smile: - ## What else can I do? + ## Actions There are action buttons at the bottom of each post. @@ -1504,15 +1504,15 @@ en: You can also **share** a link to a post, or **bookmark** it for later reference on your user page. - ## Who is talking to me? + ## Notifications When someone replies to your post, quotes your post, or mentions your `@username`, a number will immediately appear at the top right of the page. Use it access your **notifications**. - Don't worry about missing a reply – you'll be emailed replies (and messages) if you aren't online when they arrive. + Don't worry about missing a reply – you'll be emailed notifications if you aren't online when they arrive. - ## When are conversations new? + ## Your Preferences By default all conversations less than two days old are considered new, and any conversation you've participated in (replied to, created, or read for an extended period) will automatically be tracked. @@ -1522,7 +1522,7 @@ en: You can change the individual notification state of a topic via the control at the bottom of the topic (this can also be set per category). To change how you track topics, or the definition of new, see [your user preferences](%{base_url}/my/preferences). - ## Why can't I do certain things? + ## Community Trust New users are somewhat limited for safety reasons. As you participate here, you'll gain the trust of the community, become a full citizen, and those limitations will automatically be removed. At a high enough [trust level](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924), you'll gain even more abilities to help us manage our community together. diff --git a/public/images/welcome/notification-panel.png b/public/images/welcome/notification-panel.png index cd0550ec919a09d0ace1cb303454e42d2a6b623e..cbf6be4d8345740a2b93c373be0338a72af58f02 100644 GIT binary patch literal 3729 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 literal 3885 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 diff --git a/public/images/welcome/progress-bar.png b/public/images/welcome/progress-bar.png index 431d24b1c01db6fb2fbf27262d054b0b2954bb9e..aeff46a2f8ff580a764949dbe00fafe9df20795a 100644 GIT binary patch delta 1073 zcmXw(e=ys37{|YU%zE0c^P{s;v%B^dwY6>|-5o276?1x~hncfd*3#WJ*-cX|>bs`f z`lZ(d9a=M1;mW%6bAq~{OegV)uMZOhOPW*&X?>O;A*s*4-QAvh-TQf-_xsQ1?y>Cb z*-^ZYbRY=A;mS8I1YtKCdm=uZpa4P135q7DF@hQ=s7XRFK?oR~CQ2BTM46JPR=H8Y zO(1s@OnTm$cB@tswHg)$4l@YkuM5Ul)MTJQ9z=nl>SSgY+#_p z&2n)wEor00WY@T)V@%RD&R1CZ3M^FM!WnDf92Tl@#h=UN3S-4Qu2NacwYW-;CDLip z+*YSzRiwj>dMrY?MTaE@EJj#@@ZivdY+zg_og7rC24|Eq#Wa*oLNEpCDag!oN;o$Q zRkKh#5A~`+gk=ar9jS%dWvJIc!!k4=h|~~iAksp#2+cy<*TSCMJeWX1+9#te&QlhKSZHW@LtGCfXKcb_2aeWQ+Z;xDEcM(uVJ z#4h_^%v>baeH}*HKfSkmWBqwe6kKMr_lBpvy6xH@(Ry&hvrgHPBZ_$YTlY5=Dd|F& z*#|#3pBuK%d)7I6aij9`J2M_$*Nzyc*RS!sR()4_hFS-59DV1$<-shr zXBc}{#Q)o;>4@6j{KoB?o*wU5+j;gt-co{BY(ekoHIGDxkks|`0Kd31G|ru|>uY|g z;p9m6dCu>$1Gy58{|ocgI=2Zz=d?L<;X7f;F|4f|%VOD-dAyKRcFRdqw)AYB&lGa0IRU7=^M}nHKUVJ65&THEJT~mMy_%I1|Lnli|S7k4W zx=-aFmD>*V?{Cw#*Q`Iac#q|W{FG6^dbxdp*_$j^tq%VguAh8CXOhu}{QATXV@5t} ib=>^_VmnhUq@APd_6_~u=SP|6KtzSd9B&Fc&HoqOD}_Y> delta 1521 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 literal 15028 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 Date: Wed, 10 Jun 2015 18:56:44 +1000 Subject: [PATCH 37/60] FIX: s3 cdn would break cooking if tag had no src --- lib/pretty_text.rb | 1 + spec/components/pretty_text_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 2f6417f894..d5786c147f 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -224,6 +224,7 @@ module PrettyText def self.add_s3_cdn(doc) doc.css("img").each do |img| + next unless img["src"] img["src"] = img["src"].sub(Discourse.store.absolute_base_url, SiteSetting.s3_cdn_url) end end diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 66a64a404b..5da48e1e93 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -323,8 +323,9 @@ describe PrettyText do SiteSetting.s3_upload_bucket = "test" SiteSetting.s3_cdn_url = "https://awesome.cdn" - raw = " Date: Wed, 10 Jun 2015 19:27:29 +1000 Subject: [PATCH 38/60] FEATURE: expanded error reporting in logs - add hostname - add process_id --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index e50f2484f3..c93cc92661 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -147,7 +147,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (0.8.1) + logster (0.8.2) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From 7bc3a6fff038012e730da4554435625160dd3961 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 10 Jun 2015 18:10:46 +0530 Subject: [PATCH 39/60] Update Translations --- config/locales/client.ar.yml | 1 - config/locales/client.bs_BA.yml | 1 - config/locales/client.cs.yml | 11 +++++- config/locales/client.da.yml | 11 +++--- config/locales/client.de.yml | 2 +- config/locales/client.es.yml | 4 +- config/locales/client.fi.yml | 1 - config/locales/client.fr.yml | 2 +- config/locales/client.he.yml | 66 ++++++++++++++++++++++----------- config/locales/client.it.yml | 5 ++- config/locales/client.nb_NO.yml | 1 - config/locales/client.pt_BR.yml | 1 - config/locales/client.ru.yml | 21 ++++++++++- config/locales/client.te.yml | 1 - config/locales/client.tr_TR.yml | 1 - config/locales/client.zh_TW.yml | 30 ++++++++++++++- 16 files changed, 117 insertions(+), 42 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index b0c09ba491..6379bc65eb 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -1398,7 +1398,6 @@ ar: this_month: "هذا الشهر" this_week: "هذا الاسبوع" today: "اليوم" - other_periods: "الاطلاع على مواضيع زيادة" browser_update: 'للأسف, متصفحك قديم لكي يفتح هذه الصفحة. Please قم بتحديث متصفحك.' permission_types: full: "انشاء / رد / مشاهدة" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 3108b32d5d..d6e30d9846 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -1074,7 +1074,6 @@ bs_BA: this_month: "Ovog mjeseca" this_week: "Ove nedelje" today: "Danas" - other_periods: "pogledaj još tema" browser_update: 'Nažalost, vaš internet browser je prestar za ovaj korišćenje ovog foruma. Idite na i obnovite vaš browser.' permission_types: full: "Kreiraj / Odgovori / Vidi" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 06f92da1f9..5eaf7317ca 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -420,6 +420,7 @@ cs: change_avatar: title: "Změňte si svůj profilový obrázek" gravatar: "Založeno na Gravataru" + gravatar_title: "Změňte si avatar na webových stránkách Gravatar" refresh_gravatar_title: "Obnovit Gravatar" letter_based: "Systémem přidělený profilový obrázek" uploaded_avatar: "Vlastní obrázek" @@ -555,6 +556,7 @@ cs: title: "Registrační IP adresa" avatar: title: "Profilový obrázek" + header_title: "profil, zprávy, záložky a nastavení" title: title: "Titul" filters: @@ -589,6 +591,7 @@ cs: read_only_mode: enabled: "Stranka je nastavena jen pro čtení. Můžete pokračovat v prohlížení ale interakce nemusí fungovat." login_disabled: "Přihlášení je zakázáno jelikož fórum je v režimu jen pro čtení." + too_few_topics_notice: "Vytvořte alespoň 5 veřejných témat a %{posts} veřejných příspěvků aby se nastartovala diskuze. Noví uživatelé bez obsahu co by mohli číst nezískají důvěryhodnost. Tato zpráva se zobrazuje jenom redakci." learn_more: "více informací..." year: 'rok' year_desc: 'témata za posledních 365 dní' @@ -1019,6 +1022,7 @@ cs: banner_note: "Uživatelé mohou odmítnout banner jeho zavřením. V jeden moment může být pouze jedno téma jako banner." already_banner: zero: "Žádné téma není jako banner." + one: "V současnosti je zde zakázané téma." inviting: "Odesílám pozvánku..." automatically_add_to_groups_optional: "Tato pozvánka obsahuje také přístup do této skupiny: (volitelné, pouze administrátor)" automatically_add_to_groups_required: "Tato pozvánka obsahuje také přístup do těchto skupin: (Vyžadováno, pouze administrátor)" @@ -1109,6 +1113,10 @@ cs: few: "(post withdrawn by author, will be automatically deleted in %{count} hours unless flagged)" other: "(post withdrawn by author, will be automatically deleted in %{count} hours unless flagged)" expand_collapse: "rozbalit/sbalit" + gap: + one: "zobrazit 1 skrytou odpověď" + few: "zobrazit {{count}} skryté odpovědi" + other: "zobrazit {{count}} skrytých odpovědí" more_links: "{{count}} dalších..." unread: "Příspěvek je nepřečtený." has_replies: @@ -1287,6 +1295,7 @@ cs: last: "Poslední revize" hide: "Schovejte revizi" show: "Zobrazte revizi" + comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: title: "Vykreslený příspěvek se změnami zobrazenými v textu" @@ -1310,6 +1319,7 @@ cs: create: 'Nová kategorie' save: 'Uložit kategorii' slug: 'Odkaz kategorie' + slug_placeholder: '(Dobrovolné) podtržená URL' creation_error: Během vytváření nové kategorie nastala chyba. save_error: Během ukládání kategorie nastala chyba. name: "Název kategorie" @@ -1517,7 +1527,6 @@ cs: this_month: "Tenhle měsíc" this_week: "Tenhle týden" today: "Dnes" - other_periods: "další nejlepší témata" browser_update: 'Bohužel, váš prohlížeč je příliš starý, aby na něm Discourse mohl fungovat. Prosím aktualizujte svůj prohlížeč.' permission_types: full: "Vytvářet / Odpovídat / Prohlížet" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 58f8f43fae..c9beeb6974 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -430,14 +430,14 @@ da: instructions: "Unikt, ingen mellemrum, kort" short_instructions: "Folk kan benævne dig som @{{username}}" available: "Dit brugernavn er tilgængeligt" - global_match: "E-mail tilsvarende det registrerede brugernavn" + global_match: "E-mail svarer til det registrerede brugernavn" global_mismatch: "Allerede registreret. Prøv {{suggestion}}?" not_available: "Ikke ledigt. Prøv {{suggestion}}?" too_short: "Dit brugernavn er for kort" too_long: "Dit brugernavn er for langt" checking: "Kontrollerer om brugernavnet er ledigt…" enter_email: 'Brugernavn fundet; indtast tilsvarende e-mail' - prefilled: "E-mail tilsvarende dette registrerede brugernavn" + prefilled: "E-mail svarer til dette registrerede brugernavn" locale: title: "sprog" instructions: "Brugerinterface sprog. Det skifter når de reloader siden." @@ -609,10 +609,10 @@ da: reset: "Nulstil adgangskode" complete_username: "Hvis en konto matcher brugernavnet %{username}, vil du om lidt modtage en email med instruktioner om hvordan man nulstiller passwordet." complete_email: "Hvis en konto matcher %{email}, vil du om lidt modtage en email med instruktioner om hvordan man nulstiller passwordet." - complete_username_found: "Vi fandt ingen kontoer tilsvarende brugernavnet %{username}, du burde modtage en e-mail med instruktioner om hvordan du nulstiller din adgangskode, i løbet af kort tid." - complete_email_found: "Vi har fundet en konto tilsvarende %{email}, du burde modtage en e-mail med instruktioner om hvordan du nulstiller din adgangskode, i løbet af kort tid." + complete_username_found: "Vi fandt en konto der svarer til brugernavnet %{username}, du burde i løbet af kort tid modtage en e-mail med instruktioner om hvordan du nulstiller din adgangskode." + complete_email_found: "Vi har fundet en konto der svarer til %{email}, du burde i løbet af kort tid modtage en e-mail med instruktioner om hvordan du nulstiller din adgangskode." complete_username_not_found: "Ingen kontoer passer til brugernavnet %{username}" - complete_email_not_found: "Ingen kontoer tilsvarende %{email}" + complete_email_not_found: "Ingen kontoer svarer til %{email}" login: title: "Log ind" username: "Bruger" @@ -1461,7 +1461,6 @@ da: this_month: "Denne måned" this_week: "Denne uge" today: "I dag" - other_periods: "Se flere top emner" browser_update: 'Desværre, din browser er for gammel til at kunne virke med dette forum. Opgradér venligst din browser.' permission_types: full: "Opret / Besvar / Se" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index dc1e737e94..89b0574990 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -1460,7 +1460,7 @@ de: this_month: "Dieser Monat" this_week: "Diese Woche" today: "Heute" - other_periods: "zeige weitere angesagte Themen" + other_periods: "zeige angesagte Themen:" browser_update: 'Dein Webbrowser ist leider zu alt, um dieses Forum zu besuchen. Bitte installiere einen neueren Browser.' permission_types: full: "Erstellen / Antworten / Ansehen" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index d60fcd620c..4d0f2b20e5 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -867,7 +867,7 @@ es: toggle_information: "detalles del tema" read_more_in_category: "¿Quieres leer más? Consulta otros temas en {{catLink}} o {{latestLink}}." read_more: "¿Quieres leer más? {{catLink}} o {{latestLink}}." - read_more_MF: "Hay { UNREAD, plural, =0 {} one { 1 no leído } other { are # no leídos } } { NEW, plural, =0 {} one { {BOTH, select, true{y } false { } other{}} 1 nuevo tema} other { {BOTH, select, true{y } false { } other{}} # nuevos temas} } restantes, o {CATEGORY, select, true {explora otros temas en {catLink}} false {{latestLink}} other {}}" + read_more_MF: "Hay { UNREAD, plural, =0 {} one { 1 no leído } other { # no leídos } } { NEW, plural, =0 {} one { {BOTH, select, true{y } false { } other{}} 1 nuevo tema} other { {BOTH, select, true{y } false { } other{}} # nuevos temas} } restantes, o {CATEGORY, select, true {explora otros temas en {catLink}} false {{latestLink}} other {}}" browse_all_categories: Ver todas las categorías view_latest_topics: ver los temas recientes suggest_create_topic: ¿Por qué no creas un tema? @@ -1462,7 +1462,7 @@ es: this_month: "Este mes" this_week: "Esta semana" today: "Hoy" - other_periods: "ver más temas top" + other_periods: "ver temas top" browser_update: 'Desafortunadamente, tu navegador es demasiado antiguo para funcionar en este sitio. Por favor actualízalo.' permission_types: full: "Crear / Responder / Ver" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index b56593412d..80b1c23a2e 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -1458,7 +1458,6 @@ fi: this_month: "Tässä kuussa" this_week: "Tällä viikolla" today: "Tänään" - other_periods: "näytä lisää huippuketjuja" browser_update: 'Valitettavasti tätä sivustoa ei voi käyttää näin vanhalla selaimella. Ole hyvä ja päivitä selaimesi.' permission_types: full: "Luoda / Vastata / Nähdä" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 9b6e02537c..19264e95d7 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -1466,7 +1466,7 @@ fr: this_month: "Ce mois-ci" this_week: "Cette semaine" today: "Aujourd'hui" - other_periods: "voir plus de meilleurs sujets" + other_periods: "voir le top" browser_update: 'Malheureusement, votre navigateur est trop vieux pour ce site. Merci de mettre à jour votre navigateur.' permission_types: full: "Créer / Répondre / Voir" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 18593486c0..c6b7c5adc3 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -379,6 +379,7 @@ he: set_password: "הזן סיסמה" change_about: title: "שינוי בנוגע אליי" + error: "הייתה שגיאה בשינוי הערך" change_username: title: "שנה שם משתמש" confirm: "אם תשנו את שם המשתמש/ת שלך, כל הציטוטים של ההודעות שלך ואזכורי @שם_המשתמש שלך יישברו. את/ה בטוחים לחלוטין שברצונך לשנות?" @@ -393,6 +394,7 @@ he: change_avatar: title: "שינוי תמונת הפרופיל" gravatar: "Gravatar, מבוסס על" + gravatar_title: "שנה את ה-avatar שלך באתר-Gravatar" refresh_gravatar_title: "רענון האווטר שלכם" letter_based: "תמונת פרופיל משובצת מערכתית" uploaded_avatar: "תמונה אישית" @@ -524,6 +526,7 @@ he: title: "כתובת IP בהרשמה" avatar: title: "תמונת פרופיל" + header_title: "פרופיל, הודעות, סימניות והגדרות " title: title: "כותרת" filters: @@ -682,6 +685,7 @@ he: title_placeholder: " במשפט אחד, במה עוסק הדיון הזה?" edit_reason_placeholder: "מדוע ערכת?" show_edit_reason: "(הוספת סיבת עריכה)" + reply_placeholder: "הקלד כאן. השתמש בMarkdown BBCode או HTML לערוך. גרור והדבק תמונה כדי להעלות אותה." view_new_post: "הצגת את ההודעה החדשה שלך." saving: "שומר..." saved: "נשמר!" @@ -743,7 +747,7 @@ he: linked: "

{{username}} {{description}}

" granted_badge: "

הרוויח/ה '{{description}}'

" popup: - mentioned: '{{username}} הזכיר אוך ב"{{topic}}" - {{site_title}}' + mentioned: '{{username}} הזכיר אותך ב{{topic}}" - {{site_title}}"' quoted: '{{username}} ציטט אותך ב"{{topic}}" - {{site_title}}' replied: '{{username}} הגיב לך ב"{{topic}}" - {{site_title}}' posted: '{{username}} הגיב ב"{{topic}}" - {{site_title}}' @@ -885,14 +889,14 @@ he: position: "הודעה %{current} מתוך %{total}" notifications: reasons: - '3_6': 'תקבלו התראות כיוון שאת/ה צופה בקטגוריה הזו.' - '3_5': 'תקבל/י התראות כיוון שהתחלת לצפות בנושא הזה אוטומטית.' - '3_2': 'תקבל/י התראות כיוון שאת/ה צופים בנושא הזה.' + '3_6': 'תקבלו התראות כיוון שאת/ה עוקב אחרי קטגוריה זו.' + '3_5': 'תקבל/י התראות כיוון שהתחלת לעקוב אחרי הנושא הזה אוטומטית.' + '3_2': 'תקבל/י התראות כיוון שאת/ה עוקב אחרי נושא הזה.' '3_1': 'תקבל/י התראות כיוון שאת/ה יצרת את הנושא הזה.' - '3': 'תקבל/י התראות כיוון שאת/ה צופה בנושא הזה.' + '3': 'תקבל/י התראות כיוון שאת/ה עוקב אחרי נושא זה.' '2_8': 'תקבלו התראות כיוון שאת/ה צופה בקטגוריה הזו.' '2_4': 'תקבל/י התראות כיוון שפרסמת תגובה לנושא הזה.' - '2_2': 'תקבל/י התראות כיוון שאת/ה עוקב/ת אחרי הנושא הזה.' + '2_2': 'תקבל/י התראות כיוון שאת/ה צופה אחרי הנושא הזה.' '2': 'תקבל/י התראות כיוון שקראת את הנושא הזה.' '1_2': 'תקבלו הודעה אם מישהו יזכיר את @שם_המשתמש/ת שלך או ישיב לפרסום שלך.' '1': 'תקבלו התראה אם מישהו יזכיר את @שם_המשתמש/ת שלך או ישיב לפרסום שלך.' @@ -900,17 +904,17 @@ he: '0_2': 'אתה מתעלם מכל ההתראות בנושא זה.' '0': 'אתה מתעלם מכל ההתראות בנושא זה.' watching_pm: - title: "צופה" - description: "תקבל/י התראה על כל פרסום בהודעה זו. גם סך הפרסומים החדשים ואלו שלא נקראו יופיעו לצד הנושא." + title: "עוקב" + description: "תקבל/י התראה על כל פרסום בהודעה זו. " watching: - title: "צופה" - description: "תקבל/י התראה על כל פרסום חדש תחת נושא זה. סך הפרסומים החדשים ושלא נקראו יופיע גם לצד הנושא." + title: "עוקב" + description: "תקבל/י התראה על כל פרסום חדש תחת נושא זה. " tracking_pm: - title: "עוקב" - description: "סך הפרסומים החדשים ואלו שלא נקראו יופיע לצד ההודעה. תקבל/י התראה אם מישהו/י יזכירו את @שם_המשתמש/ת שלך או יגיבו לפרסום שלך." + title: "רגיל+" + description: "כמו רגיל, כמו כן סך הפרסומים החדשים ואלו שלא נקראו יופיע לצד ההודעה." tracking: - title: "עוקב" - description: "סך הפרסום החדשים והלא נקראים יופיע לצד הנושא. תקבלו התראה אם מישהו יזכיר את @שם_המשתמש שלך או ישיב על פרסום שלך." + title: "רגיל+" + description: "כמו רגיל, כמו כן סך הפרסום החדשים והלא נקראים יופיע לצד הנושא. " regular: title: "רגיל" description: "תרבלו התראה אם מישהו יזכיר את @שם_המשתמש/ת שלך או ישיב לפרסום שלך." @@ -919,10 +923,10 @@ he: description: "תקבל/י התראה אם מישהו יזכיר את @שם_המשתמש/ת שלך או יגיב לפרסום שלך בהודעה." muted_pm: title: "מושתק" - description: "לעולם לא תקבל/י התראה לשום דבר בנוגע להודעה זו." + description: "לעולם לא תקבל/י התראה בנוגע להודעה זו." muted: title: "מושתק" - description: "לעולם לא תקבל/י התראות על הנושא הזה, והוא לא יופיע בעמוד הלא נקראו שלך." + description: "לעולם לא תקבל/י התראות על הנושא הזה, והוא לא יופיע בעמוד ה\"לא נקראו\" שלך." actions: recover: "שחזר נושא" delete: "מחק נושא" @@ -995,10 +999,13 @@ he: to_forum: "נשלח מייל קצר המאפשר לחברך להצטרף באופן מיידי באמצעות לחיצה על קישור, ללא צורך בהתחברות למערכת הפורומים." sso_enabled: "הכנס את שם המשתמש של האדם שברצונך להזמין לנושא זה." to_topic_blank: "הכנס את שם המשתמש או כתובת דואר האלקטרוני של האדם שברצונך להזמין לנושא זה." + to_topic_email: "הזנת כתובת אימייל. אנחנו נשלח הזמנה שתאפשר לחברך להשיב לנושא הזה." to_topic_username: "הכנסת את שם המשתמש של האדם שברצונך להזמין. אנו נשלח התראה למשתמש זה עם קישור המזמין אותו לנושא זה." to_username: "הכנס את שם המשתמש של האדם שברצונך להזמין. אנו נשלח התראה למשתמש זה עם קישור המזמין אותו לנושא זה." email_placeholder: 'name@example.com' + success_email: "שלחנו הזמנה ל: {{emailOrUsername}}. נודיע לך כשהזמנה תענה. בדוק את טאב ההזמנות בעמוד המשתמש שלך בשביל לעקוב אחרי ההזמנות ששלחת. " success_username: "הזמנו את המשתמש להשתתף בנושא." + error: "מצטערים, לא יכלנו להזמין האיש הזה. אולי הוא כבר הוזמן בעבר? (תדירות שליחת ההזמנות מוגבלת)" login_reply: 'התחברו כדי להשיב' filters: n_posts: @@ -1273,11 +1280,11 @@ he: parent: "קטגורית אם" notifications: watching: - title: "צופה" - description: "תצפו באופן אוטומטי בכל הנושאים החדשים בקטגוריות אלה. תקבלו התראה על כל פרסום ונושא חדש, ובנוסף, סך הפרסומים החדשים ושלא נקראו יופיעו לצד הנושא." - tracking: title: "עוקב" - description: "תעקבו באופן אוטומטי אחרי כל הנושאים החדשים בקטגוריות אלה. סך הפרסומים החדשים ושלא נקראו יופיעו לצד הנושא." + description: "תצפו באופן אוטומטי בכל הנושאים החדשים בקטגוריות אלה. תקבלו התראה על כל פרסום ונושא חדש." + tracking: + title: "רגיל+" + description: "כמו רגיל, וכן סך הפרסומים החדשים ושלא נקראו יופיעו לצד הנושא." regular: title: "רגיל" description: "תקבלו התראה אם מישהו יזכיר את @שם_המשתמש/ת שלכם או ישיב לפרסום שלך." @@ -1312,6 +1319,8 @@ he: action: "סימון נושא" topic_map: title: "סיכום נושא" + participants_title: "מפרסמים מתמידים" + links_title: "לינקים פופלארים" links_shown: "הצג את כל הקישורים {{totalLinks}}..." clicks: one: "לחיצה אחת" @@ -1347,12 +1356,21 @@ he: אחר {}} original_post: "הודעה מקורית" views: "צפיות" + views_lowercase: + one: "צפיה" + other: "צפיות" replies: "תגובות" views_long: "הנושא הזה נצפה {{number}} פעמים" activity: "פעילות" likes: "לייקים" + likes_lowercase: + one: "לייקים" + other: "לייקים" likes_long: "יש {{number}} לייקים לנושא הזה" users: "משתמשים" + users_lowercase: + one: "משתמש" + other: "משתמשים" category_title: "קטגוריה" history: "היסטוריה" changed_by: "מאת {{author}}" @@ -1372,6 +1390,9 @@ he: read: title: "נקרא" help: "נושאים שקראת, לפי סדר קריאתם" + search: + title: "חיפוש" + help: "חיפוש בכל הנושאים" categories: title: "קטגוריות" title_in: "קטגוריה - {{categoryName}}" @@ -1424,7 +1445,6 @@ he: this_month: "החודש" this_week: "השבוע" today: "היום" - other_periods: "ראו עוד נושאים מובילים." browser_update: 'למרבה הצער, הדפדפן שלכם זקן מידי מכדי לעבוד באתר זה.. אנא שדרגו את הדפדפן שלכם.' permission_types: full: "צרו / תגובה/ צפייה" @@ -1456,6 +1476,7 @@ he: admins: 'מנהלים ראשיים:' blocked: 'חסומים:' suspended: 'מושעים:' + private_messages_title: "הודעות" space_free: "{{size}} חופשיים" uploads: "העלאות" backups: "גיבויים" @@ -1560,8 +1581,11 @@ he: name: "שם" add: "הוספה" add_members: "הוספת חברים וחברות" + custom: "מותאם" + automatic: "אוטומטי" automatic_membership_email_domains: "משתמשים אשר נרשמים עם מארח דוא\"ל שתואם בדיוק לאחד מהרשימה, יוספו באופן אוטומטי לקבוצה זו:" automatic_membership_retroactive: "החלת כלל מארח דוא\"ל זהה כדי להוסיף משתמשים רשומים" + default_title: "ברירת המחדל לכל המשתמשים בקבוצה זו" api: generate_master: "ייצר מפתח מאסטר ל-API" none: "אין מפתחות API פעילים כרגע." diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index cc7a8f3c59..d7f518cc65 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -333,6 +333,7 @@ it: dismiss_notifications: "Imposta tutti come Letti" dismiss_notifications_tooltip: "Imposta tutte le notifiche non lette come lette " disable_jump_reply: "Non saltare al mio messaggio dopo la mia risposta" + dynamic_favicon: "Visualizza conteggio argomenti nuovi / creati nell'icona del browser" edit_history_public: "Consenti agli altri utenti di visualizzare le mie revisioni ai messaggi" external_links_in_new_tab: "Apri tutti i link esterni in nuove schede" enable_quoting: "Abilita \"rispondi quotando\" per il testo evidenziato" @@ -378,6 +379,7 @@ it: set_password: "Imposta Password" change_about: title: "Modifica i dati personali" + error: "Si è verificato un errore nel cambio di questo valore." change_username: title: "Cambia Utente" confirm: "Se modifichi il tuo nome utente, non funzioneranno più le precedenti citazioni ai tuoi messaggi e le menzioni @nome. Sei sicuro di volerlo fare?" @@ -392,6 +394,7 @@ it: change_avatar: title: "Cambia l'immagine del tuo profilo" gravatar: "Gravatar, basato su" + gravatar_title: "Cambia il tuo avatar sul sito Gravatar" refresh_gravatar_title: "Ricarica il tuo Gravatar" letter_based: "Immagine del profilo assegnata dal sistema" uploaded_avatar: "Immagine personalizzata" @@ -523,6 +526,7 @@ it: title: "Indirizzo IP di Registrazione" avatar: title: "Immagine Profilo" + header_title: "profilo, messaggi, segnalibri e preferenze" title: title: "Titolo" filters: @@ -1422,7 +1426,6 @@ it: this_month: "Questo mese" this_week: "Questa settimana" today: "Oggi" - other_periods: "vedi altri argomenti di punta" browser_update: 'Purtroppo il tuo browser è troppo vecchio per funzionare su questo forum. Per favore aggiorna il browser.' permission_types: full: "Crea / Rispondi / Visualizza" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index 3a8e1bacde..c1d02aad75 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -1401,7 +1401,6 @@ nb_NO: this_month: "Denne måneden" this_week: "Denne uken" today: "I dag" - other_periods: "vis flere populære emner" browser_update: 'Dessverre, Din nettleser er for gammel og fungerer ikke med dette nettstedet.. Vennligst oppgrader nettleseren din.' permission_types: full: "Opprett / Svar / Se" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 3289beb0e1..9e4aa0b89d 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -1452,7 +1452,6 @@ pt_BR: this_month: "Neste mes" this_week: "Nesta semana" today: "Hoje" - other_periods: "veja mais tópicos em alta" browser_update: 'Infelizmente, seu navegador é muito antigo para ser utilizado neste site. Por favor atualize seu navegador.' permission_types: full: "Criar / Responder / Ver" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index f8225b3a52..c1ab2c6c14 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -255,6 +255,11 @@ ru: approval: title: "Сообщения для проверки" description: "Ваше сообщение отправлено, но требует проверки и утверждения модератором. Пожалуйста, будьте терпеливы." + pending_posts: + one: "1 сообщение ожидает одобрения." + few: "{{count}} сообщений ожидают одобрения." + many: "{{count}} сообщений ожидают одобрения." + other: "{{count}} сообщений ожидают одобрения." ok: "ОК" user_action: user_posted_topic: "{{user}} создал тему" @@ -425,6 +430,7 @@ ru: set_password: "Установить пароль" change_about: title: "Изменить информацию обо мне" + error: "При изменении значения произошла ошибка." change_username: title: "Изменить псевдоним" confirm: "Если вы измените свой псевдоним, то все существующие цитаты ваших сообщений и упоминания вас по @псевдониму в чужих сообщениях перестанут ссылаться на вас. Вы точно хотите изменить псевадоним?" @@ -439,6 +445,7 @@ ru: change_avatar: title: "Изменить фон профиля" gravatar: "На основе Gravatar" + gravatar_title: "Измените свой аватар на сайте Gravatar" refresh_gravatar_title: "Обновить ваш Gravatar" letter_based: "Фон профиля по умолчанию" uploaded_avatar: "Собственный аватар" @@ -578,6 +585,7 @@ ru: title: "IP адрес регистрации" avatar: title: "Аватар" + header_title: "профиль, сообщения, закладки и настройки" title: title: "Заголовок" filters: @@ -950,6 +958,7 @@ ru: go_top: "перейти наверх" go_bottom: "перейти вниз" go: "перейти" + jump_bottom: "перейти к последнему сообщению" jump_bottom_with_number: "перейти к сообщению %{post_number}" total: всего сообщений current: текущее сообщение @@ -1148,6 +1157,11 @@ ru: many: "(сообщение отозвано автором и будет автоматически удалено в течение %{count} часов, если только на сообщение не поступит жалоба)" other: "(сообщение отозвано автором и будет автоматически удалено в течение %{count} часов, если только на сообщение не поступит жалоба)" expand_collapse: "развернуть/свернуть" + gap: + one: "просмотреть 1 скрытый ответ" + few: "просмотреть {{count}} скрытых ответов" + many: "просмотреть {{count}} скрытых ответов" + other: "просмотреть {{count}} скрытых ответов" more_links: "еще {{count}}..." unread: "Сообщение не прочитано" has_replies: @@ -1344,6 +1358,7 @@ ru: last: "Последняя версия" hide: "Скрыть редакцию" show: "Показать редакцию" + comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: title: "Отобразить сообщение с включенными добавлениями и удалениями." @@ -1446,6 +1461,8 @@ ru: notify_action: "Сообщение" topic_map: title: "Сводка по теме" + participants_title: "Частые авторы" + links_title: "Популярные ссылки" links_shown: "показать все {{totalLinks}} ссылок..." clicks: one: "1 клик" @@ -1576,7 +1593,7 @@ ru: this_month: "За месяц" this_week: "За неделю" today: "Сегодня" - other_periods: "смотреть больше обсуждаемых сообщений" + other_periods: "просмотреть выше" browser_update: 'К сожалению, ваш браузер устарел и не поддерживается этим сайтом. Пожалуйста, обновите браузер (нажмите на ссылку, чтобы узнать больше).' permission_types: full: "Создавать / Отвечать / Просматривать" @@ -2016,6 +2033,8 @@ ru: impersonate: title: "Войти от имени пользователя" help: "Используйте этот инструмент, чтобы войти от имени пользователя. Может быть полезно для отладки. После этого необходимо выйти и зайти под своей учетной записью снова." + not_found: "Пользователь не найден." + invalid: "Извините, но вы не можете представиться этим пользователем." users: title: 'Пользователи' create: 'Добавить администратора' diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index bb715389fc..cb70775663 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -1276,7 +1276,6 @@ te: this_month: "ఈ నెల" this_week: "ఈ వారం" today: "ఈ రోజు" - other_periods: "మరిన్ని అగ్ర విషయాలు చూడు" browser_update: 'దురదృష్టవశాత్తు, ఈ సైట్ లో పనిచేయడానికి మీ బ్రౌజర్ చాలా పాతది . దయచేసి మీ బ్రౌజర్ ని నవీకరించండి.' permission_types: full: "సృష్టించి / జవాబివ్వు / చూడు" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 534de90325..efe5e9a016 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -1392,7 +1392,6 @@ tr_TR: this_month: "Bu ay" this_week: "Bu hafta" today: "Bugün" - other_periods: "daha fazla konuya bak" browser_update: 'Malesef, tarayıcınız bu site için çok eski. Lütfen tarayıcınızı güncelleyin.' permission_types: full: "Oluştur / Cevapla / Bak" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 7827550ab5..758a2f9434 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -184,8 +184,17 @@ zh_TW: none: "沒有要回顧的文章。" edit: "編輯" cancel: "取消" + view_pending: "觀看等待審核的貼文" + has_pending_posts: + one: "本主題仍有 1 篇貼文等待審核" + many: "本主題仍有 {{count}}篇貼文等待審核" confirm: "儲存變更" + delete_prompt: "您確定要刪除 %{username} 這個帳號嗎?這會同時將該帳號的所有貼文一併刪除,並封鎖他的電子郵件與 IP。" approval: + title: "貼文需等待審核" + description: "貼文已經送出,但必須等待管理者審核過後才會出現在板上,請耐心等候。" + pending_posts: + other: "你有 {{count}} 篇貼文在等待審核中" ok: "確定" user_action: user_posted_topic: "{{user}} 建立了 此討論話題" @@ -246,6 +255,7 @@ zh_TW: '11': "編輯" '12': "送出的項目" '13': "收件匣" + '14': "等待中" categories: all: "所有分類" all_subcategories: "所有" @@ -356,12 +366,15 @@ zh_TW: change_avatar: title: "設定個人資料圖片" gravatar: "Gravatar, based on" + gravatar_title: "在 Gravatar 網站修改你的頭像" refresh_gravatar_title: "重新整理你的 Gravatar 頭像" letter_based: "系統分配的個人資料圖片" uploaded_avatar: "自訂圖片" uploaded_avatar_empty: "新增一張自訂圖片" upload_title: "上傳你的圖片" upload_picture: "上傳圖片" + image_is_not_a_square: "警告:我們裁切了你的圖片,因為該圖片不是正方形的。" + cache_notice: "你已經成功更改頭圖,但由於瀏覽器快取的關係,可能要一會兒後才會顯示新頭圖。" change_profile_background: title: "基本資料背景圖案" instructions: "個人資料背景會被置中,且默認寬度為850px。" @@ -419,6 +432,8 @@ zh_TW: every_three_days: "每三天" weekly: "每週" every_two_weeks: "每兩星期" + email_direct: "當有人引用、回覆我的發文,或以 @用戶名稱 提到我時,請以電子郵件通知我。" + email_private_messages: "當有人寄給我私人訊息時,以電子郵件通知我。" email_always: "不要因為我在網站上活動而不寄信通知。" other_settings: "其它" categories_settings: "分類" @@ -461,6 +476,7 @@ zh_TW: none: "你尚未邀請任何人。你可以發送個別邀請,或者透過上傳邀請名單一次邀請一群人。" text: "從檔案大量邀請" uploading: "正在上傳..." + success: "檔案已上傳成功,處理完畢後將以私人訊息通知你。" error: "上傳 '{{filename}}' 時發生問題:{{message}}" password: title: "密碼" @@ -509,6 +525,7 @@ zh_TW: logout: "已登出" refresh: "重新整理" read_only_mode: + enabled: "管理員開啟了唯讀模式。你可以繼續瀏覽網站,但一些互動功能可能無法使用。" login_disabled: "在唯讀模式下不能登入" learn_more: "進一步了解..." year: '年' @@ -523,6 +540,8 @@ zh_TW: unmute: 取消靜音 last_post: 最後一篇文章 last_reply_lowercase: 最新回覆 + replies_lowercase: + other: 回覆 summary: enabled_description: "你正在檢視此討論話題的摘要:在這個社群裡最熱門的文章。" description: "共有 {{count}} 個回覆。" @@ -628,6 +647,7 @@ zh_TW: title_placeholder: "用一個簡短的句子來描述想討論的內容。" edit_reason_placeholder: "你為什麼做編輯?" show_edit_reason: "(請加入編輯原因)" + reply_placeholder: "在此輸入。請使用 Markdown (http://markdown.tw/) 與 BBCode (http://www.bbcode.org/reference.php) 調整格式,拖曳或貼上圖片可以直接上傳。" view_new_post: "檢視你的新文章。" saving: "正在儲存..." saved: "儲存完畢!" @@ -842,6 +862,7 @@ zh_TW: title: "一般" muted_pm: title: "靜音" + description: "你將不會再收到關於此訊息的通知。" muted: title: "靜音" description: "你將不會收到任何關於此討論話題的通知,此討論話題也不會出現在你的未讀分頁裡。" @@ -862,6 +883,7 @@ zh_TW: feature: pin: "置頂主題" unpin: "取消置頂主題" + pin_globally: "全區置頂討論話題" make_banner: "討論話題橫幅" remove_banner: "移除討論話題橫幅" reply: @@ -879,6 +901,8 @@ zh_TW: success_message: '已投訴此討論話題。' feature_topic: title: "擁有這個話題" + pin: "將此討論主題在{{categoryLink}}類別中置頂" + unpin: "取消此主題在{{categoryLink}}類別的置頂狀態" already_banner: zero: "沒有頂置的話題。" inviting: "正在邀請..." @@ -898,6 +922,7 @@ zh_TW: to_forum: "我們將向你的朋友發出一封電子郵件,他不必登入,他只要按電子郵件裡的連結就可以加入此論壇。" to_topic_blank: "輸入你想邀請的用戶的用戶名稱或電子郵件地址到該討論主題" email_placeholder: '電子郵件地址' + success_username: "我們已經邀請該使用者加入此主題討論" login_reply: '登入以發表回應' filters: n_posts: @@ -1194,6 +1219,7 @@ zh_TW: notify_action: "訊息" topic_map: title: "討論話題摘要" + participants_title: "頻繁發文者" links_title: "熱門連結" links_shown: "顯示所有 {{totalLinks}} 個連結..." clicks: @@ -1310,7 +1336,7 @@ zh_TW: this_month: "本月" this_week: "本週" today: "今天" - other_periods: "看更多精選討論話題" + other_periods: "前往頂端" browser_update: '抱歉,您的瀏覽器版本太舊,無法正常訪問該站點。。請升級您的瀏覽器。' permission_types: full: "建立 / 回覆 / 觀看" @@ -1377,6 +1403,7 @@ zh_TW: agree_title: "確認此投訴為有效且正確" agree_flag_modal_title: "批准並且 ..." agree_flag_hide_post: "批准 (隱藏文章 + 送出私人訊息)" + agree_flag_hide_post_title: "隱藏此文章,並自動向此用戶送出私人訊息,要求盡快修改它" agree_flag_restore_post: "同意(還原文章)" agree_flag_restore_post_title: "回復此文章" agree_flag: "同意投訴" @@ -1525,6 +1552,7 @@ zh_TW: confirm: "你確定要回溯資料庫到以前的工作階段?" export_csv: user_archive_confirm: "你確定要下載你的文章嗎?" + success: "開始匯出,處理完畢後將以私人訊息通知你。" failed: "匯出失敗。請觀看紀錄。" rate_limit_error: "文章每天只能下載一次,請明天再試。" button_text: "匯出" From c1cf602de2def238e6008c63e269147354bd97dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Jun 2015 17:19:58 +0200 Subject: [PATCH 40/60] FIX: uploads:backfill_shas rake task --- lib/tasks/uploads.rake | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 8ff0c70e0c..3a1f178c7a 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -6,21 +6,19 @@ require "digest/sha1" task "uploads:backfill_shas" => :environment do RailsMultisite::ConnectionManagement.each_connection do |db| - puts "Backfilling #{db}" - Upload.select([:id, :sha, :url]).find_each do |u| - if u.sha.nil? + puts "Backfilling #{db}..." + Upload.where(sha1: nil).find_each do |u| + begin + path = Discourse.store.path_for(u) + u.sha1 = Digest::SHA1.file(path).hexdigest + u.save! putc "." - path = "#{Rails.root}/public/#{u.url}" - sha = Digest::SHA1.file(path).hexdigest - begin - Upload.update_all ["sha = ?", sha], ["id = ?", u.id] - rescue ActiveRecord::RecordNotUnique - # not a big deal if we've got a few duplicates - end + rescue Errno::ENOENT + putc "X" end end end - puts "done" + puts "", "Done" end ################################################################################ From bdfdbcd217c458a02978a6e3523521ee3bb137fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Jun 2015 18:15:10 +0200 Subject: [PATCH 41/60] FIX: we need the sha of the upload to create a thumbnail --- app/models/optimized_image.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index d29560ab12..3a0c5aaa82 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -8,6 +8,7 @@ class OptimizedImage < ActiveRecord::Base def self.create_for(upload, width, height, opts={}) return unless width > 0 && height > 0 + return if upload.try(:sha1).blank? DistributedMutex.synchronize("optimized_image_#{upload.id}_#{width}_#{height}") do # do we already have that thumbnail? From a52d31e25e29a9a0405113857df2ce598f0780dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Jun 2015 18:18:20 +0200 Subject: [PATCH 42/60] FIX: properly handle external image download errors --- app/models/optimized_image.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 3a0c5aaa82..a908524675 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -26,7 +26,7 @@ class OptimizedImage < ActiveRecord::Base # create the thumbnail otherwise original_path = Discourse.store.path_for(upload) if original_path.blank? - external_copy = Discourse.store.download(upload) + external_copy = Discourse.store.download(upload) rescue nil original_path = external_copy.try(:path) end From 6c7e737294f68311a715480d6f446b9d53254a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 10 Jun 2015 18:53:14 +0200 Subject: [PATCH 43/60] FIX: truncate topic image_url --- lib/cooked_post_processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index c9a53e34a5..c853a1574c 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -207,7 +207,7 @@ class CookedPostProcessor def update_topic_image(images) if @post.is_first_post? img = images.first - @post.topic.update_column(:image_url, img["src"]) if img["src"].present? + @post.topic.update_column(:image_url, img["src"][0...255]) if img["src"].present? end end From e54125b5dc0db7a51839d096fe38927ddec8abcd Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 10 Jun 2015 13:12:15 -0400 Subject: [PATCH 44/60] FIX: Endless spinner when anonymous users navigated to 404s --- .../javascripts/discourse/models/post-stream.js.es6 | 2 +- app/assets/javascripts/discourse/templates/topic.hbs | 2 +- test/javascripts/acceptance/topic-anonymous-test.js.es6 | 8 ++++++++ test/javascripts/helpers/create-pretender.js.es6 | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index c6be242fa2..61a70f5d2c 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -713,7 +713,7 @@ const PostStream = RestModel.extend({ // If the result was 404 the post is not found if (status === 404) { topic.set('errorTitle', I18n.t('topic.not_found.title')); - topic.set('notFoundHtml', result.responseText); + topic.set('notFoundHtml', result.jqXHR.responseText); return; } diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index ea25c5c0ea..a5bb12e87a 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -125,7 +125,7 @@
{{#conditional-loading-spinner condition=noErrorYet}} {{#if model.notFoundHtml}} - {{{model.notFoundHtml}}} +
{{{model.notFoundHtml}}}
{{else}}
{{model.message}}
diff --git a/test/javascripts/acceptance/topic-anonymous-test.js.es6 b/test/javascripts/acceptance/topic-anonymous-test.js.es6 index 2703ad0faa..8e5ddf45c2 100644 --- a/test/javascripts/acceptance/topic-anonymous-test.js.es6 +++ b/test/javascripts/acceptance/topic-anonymous-test.js.es6 @@ -16,6 +16,14 @@ test("Enter without an id", () => { }); }); +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"); + }); +}); + test("Enter without access", (assert) => { visit("/t/i-dont-have-access/403"); andThen(() => { diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index 247f09b299..cad384f5e2 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -179,6 +179,10 @@ export default function() { return response(403, {}); }); + this.get('/t/404.json', () => { + return response(404, "not found"); + }); + this.get('/t/500.json', () => { return response(502, {}); }); From 611b5f996e7a2b4a59ee820da0d1cf61d1552354 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 10 Jun 2015 14:36:47 -0400 Subject: [PATCH 45/60] FIX: unpinned topics shouldn't remain pinned on categories page --- app/models/category_list.rb | 27 +++++++++++++++++++++ lib/topic_query.rb | 3 ++- spec/components/category_list_spec.rb | 21 ++++++++++++++++ spec/models/category_featured_topic_spec.rb | 6 +++-- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/app/models/category_list.rb b/app/models/category_list.rb index b7868cdb0f..b5cae822b8 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -1,3 +1,5 @@ +require_dependency 'pinned_check' + class CategoryList include ActiveModel::Serialization @@ -17,6 +19,8 @@ class CategoryList prune_empty find_user_data + sort_unpinned + trim_results end private @@ -151,4 +155,27 @@ class CategoryList @all_topics.each { |ft| ft.user_data = topic_lookup[ft.id] } end end + + def sort_unpinned + if @guardian.current_user && @all_topics.present? + # Put unpinned topics at the end of the list + @categories.each do |c| + next if c.displayable_topics.blank? || c.displayable_topics.size <= latest_posts_count + unpinned = [] + c.displayable_topics.each do |t| + unpinned << t if t.pinned_at && PinnedCheck.unpinned?(t, t.user_data) + end + unless unpinned.empty? + c.displayable_topics = (c.displayable_topics - unpinned) + unpinned + end + end + end + end + + def trim_results + @categories.each do |c| + next if c.displayable_topics.blank? + c.displayable_topics = c.displayable_topics[0,latest_posts_count] + end + end end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 15af3dd515..13c3f9bb4b 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -135,9 +135,10 @@ class TopicQuery def list_category_topic_ids(category) query = default_results(category: category.id) pinned_ids = query.where('pinned_at IS NOT NULL AND category_id = ?', category.id) + .limit(nil) .order('pinned_at DESC').pluck(:id) non_pinned_ids = query.where('pinned_at IS NULL OR category_id <> ?', category.id).pluck(:id) - (pinned_ids + non_pinned_ids)[0...@options[:per_page]] + (pinned_ids + non_pinned_ids) end def list_new_in_category(category) diff --git a/spec/components/category_list_spec.rb b/spec/components/category_list_spec.rb index 104173d3b9..808abcd5d8 100644 --- a/spec/components/category_list_spec.rb +++ b/spec/components/category_list_spec.rb @@ -102,6 +102,27 @@ describe CategoryList do end end + context "with pinned topics in a category" do + let!(:topic1) { Fabricate(:topic, category: topic_category, bumped_at: 8.minutes.ago) } + let!(:topic2) { Fabricate(:topic, category: topic_category, bumped_at: 5.minutes.ago) } + let!(:topic3) { Fabricate(:topic, category: topic_category, bumped_at: 2.minutes.ago) } + let!(:pinned) { Fabricate(:topic, category: topic_category, pinned_at: 10.minutes.ago, bumped_at: 10.minutes.ago) } + let(:category) { category_list.categories.first } + + before do + SiteSetting.stubs(:category_featured_topics).returns(2) + end + + it "returns pinned topic first" do + expect(category.displayable_topics.map(&:id)).to eq([pinned.id, topic3.id]) + end + + it "returns topics in bumped_at order if pinned was unpinned" do + PinnedCheck.stubs(:unpinned?).returns(true) + expect(category.displayable_topics.map(&:id)).to eq([topic3.id, topic2.id]) + end + end + end describe 'category order' do diff --git a/spec/models/category_featured_topic_spec.rb b/spec/models/category_featured_topic_spec.rb index 4859ea7cde..38c3383a45 100644 --- a/spec/models/category_featured_topic_spec.rb +++ b/spec/models/category_featured_topic_spec.rb @@ -34,9 +34,11 @@ describe CategoryFeaturedTopic do it 'should feature stuff in the correct order' do + SiteSetting.stubs(:category_featured_topics).returns(3) category = Fabricate(:category) - _t3 = Fabricate(:topic, category_id: category.id, bumped_at: 7.minutes.ago) + t4 = Fabricate(:topic, category_id: category.id, bumped_at: 10.minutes.ago) + t3 = Fabricate(:topic, category_id: category.id, bumped_at: 7.minutes.ago) t2 = Fabricate(:topic, category_id: category.id, bumped_at: 4.minutes.ago) t1 = Fabricate(:topic, category_id: category.id, bumped_at: 5.minutes.ago) pinned = Fabricate(:topic, category_id: category.id, pinned_at: 10.minutes.ago, bumped_at: 10.minutes.ago) @@ -45,7 +47,7 @@ describe CategoryFeaturedTopic do expect( CategoryFeaturedTopic.where(category_id: category.id).pluck(:topic_id) - ).to eq([pinned.id, t2.id, t1.id]) + ).to eq([pinned.id, t2.id, t1.id, t3.id]) end end From ae52f4e776c3a466a96ba0f67edb39e8cd37b1c2 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Jun 2015 12:53:57 -0700 Subject: [PATCH 46/60] Revert "Don't limit @mention autocomplete to latin characters" This reverts commit effe83d7a961b2b0334d9aec1905559c8015c01b. --- app/assets/javascripts/discourse/lib/user-search.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 068282c75b..790a00ff09 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -88,6 +88,11 @@ export default function userSearch(options) { currentTerm = term; return new Ember.RSVP.Promise(function(resolve) { + // TODO site setting for allowed regex in username + if (term.match(/[^a-zA-Z0-9_\.]/)) { + resolve([]); + return; + } if (((new Date() - cacheTime) > 30000) || (cacheTopicId !== topicId)) { cache = {}; } From 857ff3515de6efd05f4dd892ad9f11615923a3a6 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Jun 2015 16:14:51 -0700 Subject: [PATCH 47/60] minor copyedit --- config/locales/client.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8e1e1b142e..c82164d1fa 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1131,8 +1131,8 @@ en: sso_enabled: "Enter the username of the person you'd like to invite to this topic." to_topic_blank: "Enter the username or email address of the person you'd like to invite to this topic." to_topic_email: "You've entered an email address. We'll email an invitation that allows your friend to immediately reply to this topic." - to_topic_username: "You've entered a username. We'll send a notification to that user with a link inviting them to this topic." - to_username: "Enter the username of the person you'd like to invite. We'll send a notification to that user with a link inviting them to this topic." + to_topic_username: "You've entered a username. We'll send a notification with a link inviting them to this topic." + to_username: "Enter the username of the person you'd like to invite. We'll send a notification with a link inviting them to this topic." email_placeholder: 'name@example.com' success_email: "We mailed out an invitation to {{emailOrUsername}}. We'll notify you when the invitation is redeemed. Check the invitations tab on your user page to keep track of your invites." From a14ea757a123ca9f89797d9bd6dd1b50556b9413 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 10 Jun 2015 17:07:29 -0700 Subject: [PATCH 48/60] improvements to new user welcome copy --- config/locales/server.en.yml | 32 ++++++++++-------- public/images/welcome/progress-bar.png | Bin 1083 -> 5388 bytes .../welcome/topic-list-select-areas.png | Bin 0 -> 8662 bytes .../welcome/topic-notification-control.png | Bin 0 -> 39580 bytes 4 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 public/images/welcome/topic-list-select-areas.png create mode 100644 public/images/welcome/topic-notification-control.png diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 6ce907efab..2f7854c1a0 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1460,39 +1460,39 @@ en: text_body_template: | Here are a few quick tips to get you started: - ## Just Scroll Down + ## Reading - To read more, **just keep scrolling down!** As new posts or new topics arrive, they will appear automatically. + To read more, **just keep scrolling down!** As new posts or new topics arrive, they will appear automatically – no need to refresh the page. ## Navigation - For search, your user page, or the menu, use the **icon buttons at upper right**. - - Selecting a topic title will always take you to the next unread post in the topic. To enter at the top or bottom instead, select the date or post count. + - Selecting a topic title will always take you to the next unread post in the topic. To enter at the top or bottom instead, select the date or post count instead. + + - While reading a topic, jump to the top ↑ by selecting the topic title. Select the progress bar at the bottom right for full navigation controls, or use the home and end keys. - + ## Replying - To reply … + - to the **topic in general**, use Reply at the very bottom of the topic. - - to the **topic in general**, use the Reply button at the very bottom of the topic. - - - to a **specific person**, use the Reply button on their post. + - to a **specific person**, use Reply on their post. - with **a linked topic**, use Reply as linked Topic to the right of the post. - To quote, simply select the text you wish to quote, then press any Reply button. + To insert a quote, simply select the text you wish to quote, then press any Reply button. - To ping someone in your reply, mention their name. Type `@` to begin selecting a name. + To notify someone in your reply, mention their name. Type `@` to begin selecting a name. - For [standard Emoji](http://www.emoji.codes/), just type `:` or the traditional smileys `:)` :smile: + To use [standard Emoji](http://www.emoji.codes/), just type `:` or the traditional smileys `:)` :smile: ## Actions @@ -1510,17 +1510,21 @@ en: - Don't worry about missing a reply – you'll be emailed notifications if you aren't online when they arrive. + Don't worry about missing a reply – you'll be emailed notifications that arrive when you are away. ## Your Preferences - By default all conversations less than two days old are considered new, and any conversation you've participated in (replied to, created, or read for an extended period) will automatically be tracked. + By default all conversations less than two days old are considered new, and any conversation you've actively participated in (by replying, creating the topic, or reading for an extended period) will automatically be tracked. You will see the blue new and number indicators next to these topics: - You can change the individual notification state of a topic via the control at the bottom of the topic (this can also be set per category). To change how you track topics, or the definition of new, see [your user preferences](%{base_url}/my/preferences). + You can change the individual notification state of a topic via the notification control at the bottom of the topic. You can also set notification state per category. + + + + To change these settings, see [your user preferences](%{base_url}/my/preferences). ## Community Trust diff --git a/public/images/welcome/progress-bar.png b/public/images/welcome/progress-bar.png index aeff46a2f8ff580a764949dbe00fafe9df20795a..7545a9b6cc72433b0bdb7f3772bad13ce8e4d6f0 100644 GIT binary patch literal 5388 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 diff --git a/public/images/welcome/topic-list-select-areas.png b/public/images/welcome/topic-list-select-areas.png new file mode 100644 index 0000000000000000000000000000000000000000..0652d30446b873995558cd3cb0c24486d8b49edd GIT binary patch literal 8662 zcmajFcU%)$6gC_LML@wNG-)=f2vO-t00j}L0clDJ7JBc5js+1|f`D`r+CnJOqzQy3 zLMQ?Yp?3%^lwhdR-(lVTzPJ5;@BA@y=FT(s+&ib-ne$Azu8!J8Ms7wB2y{{X(Syez z5M2f!d!0W6>@TZYnSemoFzOHP8=#PD7=$D5P$0ph?OIrRTSgeKyT~a2j&6wP2X0Y& znzI_|G${2@p{NwLYkirXCQNR+nI(d1G=q7LqxX69lx5DXNM8-T!*(azbRQw`M#75e zsc_|BjALs?U4(#s=6tq@ooIwvOf1gAJNGCa^+{=bWW-!T=?eV3z?8paS#(){(8BKI zLEzl#NEuQM7~JWWIzas`fC#kz2#x>SIf$3#H^3GC`^dZg2L10XP{P09bQxpt@3tq= z^uGl;{@=%G)bC>miRK@{tZqKgf3SmOY-;L&f4co}p{`|2Zqn~J2;YE5YhcVb6d!-t z@Lx1BbvSIM7c;z~RJPF)DT|+Hal>={1MH1e@tAyRT=y+A@EzsRRiCbd?=Du_jA*Ab z1pQwWpWxb@SI+&mzejx4WMf3su<)FgqK})^YqKyKm#HmE{j)Dyd1aH^V@?zlYp-<0 z6W?F3iuN=$y`ebh(e6g8{{z0~8_zoGCXCJjL+Xt+=J+(KL&uh3zH+8UehkWW8w%;tcl6zUt+oJ$ArF-P%18iu)@BBQ zE{Xg8>PQ)#WWnpu&7Ij0JP_|JmO0DXtKz{pD?0tDpDHu)PtP2Lg2ia*Q+)3-%<1dK z9(myJP^k)inhCDtO)(w=Z%}NpPa>-vGWFli2V`t_Ee0p`Kc|C0S50DFi|p23uQhOu zl?BDo?kh-4PL3mYzg-Mmcse4ulqGnS+DIMUyLPdn|FLEvo+4RrkU?fT<~ax4+IM@3 z7-rXH6!&@gjr2X9@i>P=%@pAijcJtA-iLFb=@y>jE7!%2Bmom&5?6n8@+0NhyUO}u z&&HEmpBX?)W6aZWHeJ+JvPuBK;Ur871d^;e6#nec!D9$HbpmJmFWQ_vAm0gYzP>8H z#>7DZ1KnKtSEPzH6>bn{&G$$zwfx>P;BJ|M7a3ITlXtmQbo&|-XyuTU6b9@IKh_~S z^iU26G+a4k!hSfK33%KCP3~`jk~<*1TIc1M7o9Y`>l1>Z8DXjl2jiI4V-DhhgPsz`jC<}>Czecugn9YJ7%$IIpU37NymzV+% z=Hid?{X9x@0o2c#KC|t^U~xRA0Dri9)0j|?+Ec1E*b1%3h>hd&Q;CqgYW$P87#K(iz=Ug4p}$<*D{CTaR<3H7)n2hUVv#qUP@VAlkTWu|J=7p* zQ&5}hKbFET@s{LfGmCQi{0#)MNi2Uf98o{P4IKSC!1EN%H;Ge*W`b6LFQ>*uAh zB#5GSen=}YN6G#_(pss8RES!0i&6mAi;U7=)T_}2pYFQoaOJy$Nq zmbz%2U^?nJPh`FX9L-MU=DKNh5!LHzo&@ab-japIQZ-ei5W?G)x8MqNSCxRMMANTk zUcQcUnb_qlBLw?xSG>Fv$!+ngx7lU=xIiukr$1C0Y5n0{`#8U-lB+v)(XC^1C@d}M zsK~EHRsmCkHkTp|PmC8#;eBUpx9jwd{I^?rWWR5D+Pg4)BLap2mIMNA!i;Fa{jVf( zysU3oK*s*@NRn#Q!2FvlRAbgQaEqb~Gla>ujs$!`Iskm`Xf-O=8NE(75Y1u#@k! z*Hp`-1|Fv1T7$L_z4$g=tVDBU(`w;m)xH-{?D4kE%bQw@9EPH=aWX+K?XcD^`)cnm z%#-ipxyI|=^kT{fp;(D~iLaQRpAR7k{%VXX(N@1{ehId(H{^2(dhvwT@dj_$#*?75ZS~L{ANXc` z;R;HQA5GvB1f8F4FJ5-Vw@KJ%VT=R1^dX`amJo9-%S$~E#IPjwnK}1sFxPw|eJO_& z9D1tZ@PikFL`_oLRqSDYHrhrI`Z0`zftHJPP#ft4#)TGo_&N(F=cnCv%@s|zSQcY> zpm*xn`CN{-N<4i!Vc38-h{S;F>nMWe6<(H*0ETwHo`}&;dlZFwQp;*~WH<(Lt(x>y zkJi`R=?H7>xIe&ENr#jTs2*eBRDMTNUxds18qVIDi*n*z@enyIf(*9=~kphXM9vcx0@sQc*W z@kd^Sq@?VqCMPL$V=g{0${S9Ei}&asn212!TXQ5@nXHO_M(w!4&Htp|4n0-=7;3%$ zhb=fRi+(43{O9+>(aGYyWm%MC2Q~RH30#uO-)Xuj{;F;j?G85%bpzA^irWk#N!g5| zU`wNId89_2r~0*1SyT?Y{g|^MF#w_iF@OB1gD%Y`pw>S zb;`iN)yXEX^H=NT;b}#;XXT!YxsTy6-Nu*=f=yM@%=|k7wx_C%I?e(Yozr>LME#7@ ztmLxkHTHesY3pNw=rK=*9o5wM?|ZXECEkqDF-Yd>+VS!!^z0a~2(+2pVa4m`HSdRw zmr5!rvP;S+#Ej)ALKi5?&!Y1()`EBlmv3fM<2nE8bVq96C3vWPF>6~N4_aj3u~HLW z6$qL~!`={yiF^_XaYY0Vi)iw}Crp=1n4fbRN<2|J;jBJ>Hd-9m7(C&v|NUun z;-m=A{A@{4*SSLBk*nVWIH4Py+dGKo0<2n6jnvh8w`1i>V>)^D+;L}_U5-Mn3%iT5 z+J{FPGxBzZes1vjdqWSjZHlZhdw&O){?zb8FCx18Z)tmd)*jM|WvTO{BpdKTjslk6 zQg@;)X2>(p{b!nUISI{fZzi;VxE@I%GaDKu&Kgn0-Iqwc?#&3Ar81lU7 z(~VX~+D1B6OWP|+k50ug_(>nB;kcJj^VErnZuOe|hd8*N=!MT7e94nDdFSM?4=2z0 zOl(FbW%X6uP|~~DpGr-awDJqn81T~ZEN_5KD@J(Y4IYlzfty!S{pboAzvWDJJt|b$8*<>!`gMaJI&y?`?UEOdG0GrVY$5>simJqnF^NL?q=9J&h z#@(HVa!C8ETb4=osohuqnnsDD>CjboK4o|*_?3@0p%?kfkK{Lh)p^0NL0krN?)K?d zk%f^m!&}i=!7IpEpXrTeJJ{rm17Z(I?F7v)0_&rl77j_IQh^@rjB(op9mNWVc(t_S zk(eX@-Bpw4J(EXat-?jOW>5nf+1%pp&a2|qUQ6BLDI+6fpPb9sLBX0EtCisDjM6^G z0kGkkZq}e8-yEtpDZW1y)^tn5bC(72aqVHTQBH&k2YSS}eD+fo$8j5b&SB8N#sl&E zMDlW)m!Ol!=5NKNU$51tL#z!vc`bXAI~i* z=89~~6S|e_cRYdoXS$AZ%Y0|8w;b-UW$p<1P;}}g{h!~_*0GDoPiI9x zSNofDbbopDf|XGAHQL`WhpB+miLu`WZCXV#WMa(=A*Nc7$bUYnoFL^e>!3Q{-JLZY zeQL(Tz<;7~tya8``k32lCF2eI;ZFB}moWF+k9?55NBzlp9E39J)N`MTi@~cUi3c90 zONkZP%_a}Y5~kjXrZr;96CPhLYkuQE)Vje}92_|Y{B2y;AM*8BSO_~2tM0d?eInav z>(W8$r}LI#@gRP&b@c&izaFwm$k)$6%{-Q~aiA1ZnMxwZy5c|FM$#cLWon1uz zueF1RV%^PNo2(AJ5)RhMjCBG&@LG|q4;SBKb&F%GlZ3pcL`S%`*e5b}o>MtZSb*zo`9D^rM+>u*o%eL29h=yjp@!$#1De`5 zDlq=}f?d)jR-Jz^;hZSGUrC{{)smWL2*iftzqG45YEVIbVBak4qN)=%8O1#rDT?NgQB41X{7 z9+SWJlQL1TDJOQkY!f-6CXd~&obTm8t(PbmPmUZ6KnF>CM2MC1{rN*MOKwzgz=nd? zpGxNXiEMD=O;bA#%VHQ3-&_2J>cfCRqAf~BC=7{-~$Kru0kOxL~7S8 zS;<(l6w4E{<+*{e*c}RX#k`oa}2Hy$a((ly-e?E9q#};j87t)Y#|;m}yi3xoo#cYucy|NM>g%RfoLi z?F<}C9$!3vr{#)@GWWrC%~sch?!Pqak{FD7xE(OjS?%TJX+j^ zbDvP&iq|bNYVo@Bvdqrua3x>R40HYZ#LLFkK8`)@u+(D{8TPW9Bg(~#gTTX^I;n+T z%y)&!V+FZO%Ig(U-bvlQEoCB-&>^^OS4yg`Ef`jDO6{CGXFeMwlLR|~ceN3j&#Ky%yJdgF7^Ggnkhb}MoTV4{t!v3>7UofdVf_y{KMnGc zP#Ojw9tXjbv3_=$Ial_HoIoK+*5@@V`(JCQ;&GH7-_ao|=*3ozhp>oVlXW`=WQ^n+ zuZ0?W3>||#9F}v^0zDJsc==}M?kOqqB-2hLDc<>58@EvS6{*Gxd7*%f!t+Ab`VZSZ zgv%$X;}^NbX+ns&c=V#Rx+{B+$guDpvlo7Ah{O(F(tfZ0^ss^*ZSyj{y3T0^mHg^O zE-h{y1-uRFWJ@{kiLM%R4^hX%*_@sIt8C5?JX{EVe9^p1_IW$Y+4a!ZXkw-O-+`F8 z(LJ!Vpudb>8ObBPIr+wjxio*^e*f0D*g`^Cgbhw1tTiZ!a-L8owOF83_31puEq+Dg@WG&Z7J@ht^Sje~W_&wU1E}h6Ey;cEv7H{JUAK@!o(NB*fwtJ{C=S>W+`vfdb_R zrgY$i(Axu_O1$r4FIQ}9&Q@)_h(F(kd6xRibd**$G}Q`UCTKT1O+6ppN?l!S-vkmu zuUn#hyU|uvH`T0N>=AV}^UYu7wI@pkjU2ucLXX6~@E*T^envG|n$ZsMd$zSZZx13Jt^m>Gc){tU{eyz6Lq*Pmx-2Wb$)U8FVD#LzIoyC7-FQY~Stg?VO-Z=B**UyKH zXq)@^xJV`A;?Wm)IpDhlScbwhJ=PXbH3BYivpKrXQcU2mFSXx>uegZwqirtN1$Oy6 z`2{^??~Lyo{$SQE!&R!axNvJc`HvfpJZ=%EA5ew}0~?1o4uP)lPGvp;)}0%v1$JUO ztsXMLZQqy+J>bMJ3Wegu-ZSwWP9!QBe9AvOF^*qf@iyvlgc87lmtC~2x33p)Fqia% zjR}M}9~AM)D>WMz)?G}pQ>*iPNh+SH^6%&-o*|UY1zz^KWTa#)#<0Ef=jMIIYSKvU z-}ZMC913q6njfG$E$^<)1NK)nUdt)kd(xC+Kbl1Y_U-&?f#IH-SQ|fP41i%U?4C3S z_M>+SpXZWBEc|LSFRmAi6yMI=H-cHKBRAx*P?K|^ejKEcSo}K}mJV@|*}`8V>PTYL zx22}PjH@SMPS6w1L6Rnh7JY)VeGqWzIi`{YA>6z@Nol-%q}Ch6{S3C*6PY zKOe)iUXz9I1k}%?6U{=k&n#=6d8QxcxE)lq^#1K;wJf*1x+Y(b07T%Aka;$A`dHtw zvm&7^U7w3%wrs}@D$vgpIau8WEfY71wvqqqMw>`_%(F|%lc5|iGIP7WOo~%JN!2F5 zg}QoKeQS*HKyfn7(O)Q*%(pS$yV}XG`n2=In9Thkc%&%395$Q3v}VUVvz2OHJ?7TUyv(Aqa7@A-6g@*THINBUCNF(34{3*PTDlC)J%xd-z9h*}JshJj5 zIl?R5bsuK8=uh%DTQt69+dj-*5(1lIZsh3zE~Qu5sM%N|$n`eYCu0J;Qf+N5$W+NZ zjIIW^VRS|5;LtD0GcnQ6YRX1}HjCd2vvAO%okyJXOK$KMS2Z+@Rj;hQN?bw7zyiR~ z$-a5=PB`gw*48dvj{Vf~j&4r*s@zgmjDvr?Nj=?mM4H2W;QQYFptVH+jZ1s9d0q1d zY2}40-iWx53k%5>#vSqX^f=f0tN9Gs(`5)-{CO$d{XWCr4tqVdHSzH0ws?c;8%C87 z%}(APMZ4jQtSY>zui^1+Ifw%hhOO&3?B?5_nAo#gA5N|s;wm@_v2xYPm8FCq_V(De z>(=Y;JP_mS$**W=S-v_fa>1e1;oJT9D9&>iqn*3zO}N)~Jm9=NM{%rsvZ^1M_680a z?W>xDY2_3thiAQ~S}A&7Q^R}W1ZBY6d-4U9x~5Wgt-g}N_SUoZ9yd{Q{p z=Tplrx{9Szx~gR7P}0F3nuSJg@ICDHpydLFPvLJ+t$jZrE3SB;%xL(73cdb5Q7ZH< z$VUh$c>^`?@JO}#D`ZrVoJ2io4|@8sF_1lYr;X}KgLHg&QzZs1>=dbgcrWdnkI5UG zKk&JkOv53{Y25EuP&odWvKGi4J_P494M?h)+GqS61iCu_yoxD>e=|MVNSx`;X-^31 zy`W|Kl#{(f|D{)6yjLo@pGFjXCQbzY^LR*w_1dH1^3M`FF&FbA8=gUOrvJP8<9Ihs z6^eyxSIX=z%gUO3V=2(brQW3h{c3y(jJxrZKS07T2wsV1lL=in;BAVeSn;WBVhL*P zgd4o-CG8`{|jPYlZ4%+T<4tE|7nX1FMQRM+CX3&(t;gCs9VUrWL1dqzypph8F$$FzvBIUS&Q9+I5Mj1xvd zzYgiPE=K44mVm7C5j#zuexQl+J1@t-cgiY0JHp6_XDGjkV9o`~{OM~F|CbZ4(n!Gi zA;8iK-0))z4^)Y*{n}S`!Wh%ik^?3em^^O$FXvGfCyiQ=|rW+pQB0luG{_m>&jQsw4U3jAxV7ZDM9WW%@m0m*0tg|8=vQ-$AdsA9hvN(R2 zPNy)B_+3+Uydtr|=zqS(0z9^RL3kk=&_A;QEdQIi0i|c@i;z&jBqwK`*7u!U%Fg_Q zm{ykK2jWKdS8!acG*5bLZ?y8$*i{%MrlB+i^qnU95(20HB@3ah9CapHN z{jAv7eXpA!5Wvuca5|%oBsT_H$-?n%P7nzB^yg%KJ5go2TZ>#- z9xGoQf`p=RPiA<;Yt{kY&qb^t&&9_`Dk?(|@V|wCz{5X}?>ZcFB3O9NyV@*w99{4> zVzbu~MElIupzAmIQLdHJuN*-kxSRV0POp7VtcrpA8e>*+9wu-0!Lj^je5_we56~eP zcXqnico8v82#h&cEWqk{*E#ViY(=JvD)%X;DqgYAvrFDJqFbjh6(X;*fI_CTvwVTj zrD&P&7)!|eu5z!Sc0V40pTwBGTgZA;aLNy78%3KR*qBt_05~s^zkaIw?>Fp^ukAZw z`JK6@yZeO_JG6>eWHc;)CW|T|ZP+-ZHT9{Lz#vGm@(tD{^_~p$w)WzH_AzS^DBYLi z_-+%4JktuIXo-%ejK=IqZIY_xRrpf|t{Vg&=!7!6g@_}8fu4GeDW}FJTfm2D>Q)JV z2#kn<>kd>8Dzb*(n7wEU4*U17fP0Sr{(kTe zw{!~s&A9>({@-5=T=6W>f7}iPTDtWwxRHu~dx{0Nm49LX{l@VB_4EJgcRHA+=C4}9 Ur3Y_9Py10<)p<~){OtAr0AwD)P5=M^ literal 0 HcmV?d00001 diff --git a/public/images/welcome/topic-notification-control.png b/public/images/welcome/topic-notification-control.png new file mode 100644 index 0000000000000000000000000000000000000000..71b2b1d1bdd5096b271a6cb16834e6da497c754c GIT binary patch literal 39580 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