From df81d109e5183c09eac7ae64cbab7f96bc71b9a8 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 28 Sep 2017 16:08:14 -0400 Subject: [PATCH 001/108] The ability to attach `attrs` when embedding widgets --- .../discourse/widgets/header-contents.js.es6 | 4 +-- lib/javascripts/widget-hbs-compiler.js.es6 | 35 ++++++++++++++++--- script/test_hbs_compiler.rb | 28 +++++++++++++++ test/javascripts/widgets/widget-test.js.es6 | 2 +- 4 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 script/test_hbs_compiler.rb diff --git a/app/assets/javascripts/discourse/widgets/header-contents.js.es6 b/app/assets/javascripts/discourse/widgets/header-contents.js.es6 index 819178b6ac..e998053ff4 100644 --- a/app/assets/javascripts/discourse/widgets/header-contents.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header-contents.js.es6 @@ -4,10 +4,10 @@ import hbs from 'discourse/widgets/hbs-compiler'; createWidget('header-contents', { tagName: 'div.contents.clearfix', template: hbs` - {{attach widget="home-logo"}} + {{attach widget="home-logo" attrs=attrs}}
{{yield}}
{{#if attrs.topic}} - {{attach widget="header-topic-info"}} + {{attach widget="header-topic-info" attrs=attrs}} {{/if}} `, }); diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index 3f96a64eaa..07d418ff2b 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -1,20 +1,45 @@ function resolve(path) { if (path.indexOf('settings') === 0) { return `this.${path}`; - } else if (path.indexOf('parentState') === 0) { - return `attrs._${path}`; } - return path; } +function sexp(value) { + if (value.path.original === "hash") { + + let result = []; + + value.hash.pairs.forEach(p => { + result.push(`"${p.key}": ${p.value.original}`); + }); + + return `{ ${result.join(", ")} }`; + } +} + +function argValue(arg) { + let value = arg.value; + if (value.type === "SubExpression") { + return sexp(arg.value); + } else if (value.type === "PathExpression") { + return value.original; + } +} + function mustacheValue(node, state) { let path = node.path.original; switch(path) { case 'attach': - const widgetName = node.hash.pairs.find(p => p.key === "widget").value.value; - return `this.attach("${widgetName}", state ? $.extend({}, attrs, { _parentState: state }) : attrs)`; + let widgetName = node.hash.pairs.find(p => p.key === "widget").value.value; + + let attrs = node.hash.pairs.find(p => p.key === "attrs"); + if (attrs) { + return `this.attach("${widgetName}", ${argValue(attrs)})`; + } + return `this.attach("${widgetName}", attrs)`; + break; case 'yield': return `this.attrs.contents()`; diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb new file mode 100644 index 0000000000..1b846615dc --- /dev/null +++ b/script/test_hbs_compiler.rb @@ -0,0 +1,28 @@ +template = <<~HBS + {{attach widget="widget-name" attrs=attrs}} + {{#if state.category}} + {{attach widget="category-display" attrs=(hash category=state.category)}} + {{/if}} +HBS + +ctx = MiniRacer::Context.new(timeout: 15000) +ctx.eval("var self = this; #{File.read("#{Rails.root}/vendor/assets/javascripts/babel.js")}") +ctx.eval(File.read(Ember::Source.bundled_path_for('ember-template-compiler.js'))) +ctx.eval("module = {}; exports = {};"); +ctx.attach("rails.logger.info", proc { |err| puts(err.to_s) }) +ctx.attach("rails.logger.error", proc { |err| puts(err.to_s) }) +ctx.eval < Date: Thu, 28 Sep 2017 16:15:24 -0400 Subject: [PATCH 002/108] Fix ruby lint error --- script/test_hbs_compiler.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb index 1b846615dc..a6ef433592 100644 --- a/script/test_hbs_compiler.rb +++ b/script/test_hbs_compiler.rb @@ -25,4 +25,3 @@ ctx.eval(source) js_source = ::JSON.generate(template, quirks_mode: true) puts ctx.eval("exports.compile(#{js_source})"); - From a5db7ba25a62154eef878f0e5b9eb94a2ca65d36 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 29 Sep 2017 08:19:35 +0800 Subject: [PATCH 003/108] Opps no reason to limit this to 1. --- config/sidekiq.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/sidekiq.yml b/config/sidekiq.yml index a3ed5fc5f0..3f94e6a21b 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,6 +1,6 @@ --- development: - :concurrency: 1 + :concurrency: 5 :queues: - [critical,4] - [default, 2] From 8dae98a3f65b7533aa567f7f0edeacb28624b55f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 29 Sep 2017 08:32:19 +0800 Subject: [PATCH 004/108] Skip randomly failing test on Travis for now. --- .../connection_adapters/postgresql_fallback_adapter_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index 036c990d89..924e44390e 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -94,6 +94,7 @@ describe ActiveRecord::ConnectionHandling do expect(postgresql_fallback_handler.master_down?).to eq(nil) expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) + skip("Figuring out why the following keeps failing to obtain a connection on Travis") expect(ActiveRecord::Base.connection) .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) end From 6baea9948be82e96adef4281a8aa410aed93f239 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 29 Sep 2017 08:35:23 +0800 Subject: [PATCH 005/108] Revert "fix the build" This reverts commit 8b74c7d325ace3ad5deaebdec50d015ca09299fc. --- app/controllers/application_controller.rb | 6 ++++++ spec/controllers/topics_controller_spec.rb | 12 +++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5f5537ae3b..cce9eb95fd 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -167,6 +167,12 @@ class ApplicationController < ActionController::Base render_json_error I18n.t(opts[:custom_message] || type), type: type, status: status_code else + begin + current_user + rescue Discourse::InvalidAccess + return render plain: I18n.t(type), status: status_code + end + render html: build_not_found_page(status_code, opts[:include_ember] ? 'application' : 'no_ember') end end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 15e6d6f606..74b02ba222 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -1059,12 +1059,14 @@ describe TopicsController do end it 'returns 403 for an invalid key' do - get :show, params: { - topic_id: topic.id, slug: topic.slug, api_key: "bad" - }, format: :json + [:json, :html].each do |format| + get :show, params: { + topic_id: topic.id, slug: topic.slug, api_key: "bad" + }, format: format - expect(response.code.to_i).to be(403) - expect(response.body).to eq(I18n.t("invalid_access")) + expect(response.code.to_i).to be(403) + expect(response.body).to eq(I18n.t("invalid_access")) + end end end end From 05dd97ffe076ef71e1e6283566506ef6d3812fa0 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 29 Sep 2017 11:48:52 +1000 Subject: [PATCH 006/108] correct iPad sizing --- .../javascripts/discourse/lib/safari-hacks.js.es6 | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 index 86a60bd835..43c70615f0 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 @@ -19,10 +19,10 @@ function calcHeight() { // iPhone shrinks header and removes footer controls ( back / forward nav ) // at 39px we are at the largest viewport - const smallViewport = (window.screen.height - window.innerHeight) > 40; + const portrait = window.innerHeight > window.innerWidth; + const smallViewport = ((portrait ? window.screen.height : window.screen.width) - window.innerHeight) > 40; - // portrait - if (window.screen.height > window.screen.width) { + if (portrait) { // iPhone SE, it is super small so just // have a bit of crop @@ -39,24 +39,22 @@ function calcHeight() { if (window.screen.height === 736) { withoutKeyboard = smallViewport ? 353 : 383; } - // iPad can use innerHeight cause it renders nothing in the footer if (window.innerHeight > 920) { withoutKeyboard -= 45; } } else { + // landscape - // // iPad, we have a bigger keyboard - if (window.innerWidth > window.innerHeight && window.innerHeight > 665) { + if (window.innerHeight > 665) { withoutKeyboard -= 128; } } // iPad portrait also has a bigger keyboard - return Math.max(withoutKeyboard, min); } From f6fdc1ebe81652be07e8c2c12b59812305de1ba5 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 29 Sep 2017 12:31:50 +1000 Subject: [PATCH 007/108] FEATURE: flexible crawler detection You can use the crawler user agents site setting to amend what user agents are considered crawlers based on a string match in the user agent Also improves performance of crawler detection slightly --- config/locales/server.en.yml | 1 + config/site_settings.yml | 3 +++ lib/crawler_detection.rb | 10 +++++++++- lib/freedom_patches/regexp.rb | 9 +++++++++ spec/components/crawler_detection_spec.rb | 8 ++++++++ 5 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 lib/freedom_patches/regexp.rb diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e289dd0208..dbcb7f8ba1 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1063,6 +1063,7 @@ en: gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF" enable_escaped_fragments: "Fall back to Google's Ajax-Crawling API if no webcrawler is detected. See https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Allow moderators to create new categories" + crawler_user_agents: "List of user agents that are considered crawlers and served static HTML instead of JavaScript payload" cors_origins: "Allowed origins for cross-origin requests (CORS). Each origin must include http:// or https://. The DISCOURSE_ENABLE_CORS env variable must be set to true to enable CORS." use_admin_ip_whitelist: "Admins can only log in if they are at an IP address defined in the Screened IPs list (Admin > Logs > Screened Ips)." blacklist_ip_blocks: "A list of private IP blocks that should never be crawled by Discourse" diff --git a/config/site_settings.yml b/config/site_settings.yml index 982d5cef5c..a145999d67 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -926,6 +926,9 @@ security: enable_escaped_fragments: true allow_index_in_robots_txt: true allow_moderators_to_create_categories: false + crawler_user_agents: + default: 'Googlebot|Mediapartners|AdsBot|curl|HTTrack|Twitterbot|facebookexternalhit|bingbot|Baiduspider|ia_archiver|Wayback Save Page|360Spider|Swiftbot|YandexBot' + type: list cors_origins: default: '' type: list diff --git a/lib/crawler_detection.rb b/lib/crawler_detection.rb index a8892fdc76..5d222ecf7b 100644 --- a/lib/crawler_detection.rb +++ b/lib/crawler_detection.rb @@ -1,9 +1,17 @@ module CrawlerDetection + # added 'ia_archiver' based on https://meta.discourse.org/t/unable-to-archive-discourse-pages-with-the-internet-archive/21232 # added 'Wayback Save Page' based on https://meta.discourse.org/t/unable-to-archive-discourse-with-the-internet-archive-save-page-now-button/22875 # added 'Swiftbot' based on https://meta.discourse.org/t/how-to-add-html-markup-or-meta-tags-for-external-search-engine/28220 + def self.to_matcher(string) + escaped = string.split('|').map { |agent| Regexp.escape(agent) }.join('|') + Regexp.new(escaped) + end def self.crawler?(user_agent) - !/Googlebot|Mediapartners|AdsBot|curl|HTTrack|Twitterbot|facebookexternalhit|bingbot|Baiduspider|ia_archiver|Wayback Save Page|360Spider|Swiftbot|YandexBot/.match(user_agent).nil? + # this is done to avoid regenerating regexes + @matchers ||= {} + matcher = (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) + matcher.match?(user_agent) end end diff --git a/lib/freedom_patches/regexp.rb b/lib/freedom_patches/regexp.rb new file mode 100644 index 0000000000..5ff804c490 --- /dev/null +++ b/lib/freedom_patches/regexp.rb @@ -0,0 +1,9 @@ +unless ::Regexp.instance_methods.include?(:match?) + class ::Regexp + # this is the fast way of checking a regex (zero string allocs) added in Ruby 2.4 + # backfill it for now + def match?(string) + !!(string =~ self) + end + end +end diff --git a/spec/components/crawler_detection_spec.rb b/spec/components/crawler_detection_spec.rb index e3956b1070..6443d84a52 100644 --- a/spec/components/crawler_detection_spec.rb +++ b/spec/components/crawler_detection_spec.rb @@ -3,6 +3,14 @@ require_dependency 'crawler_detection' describe CrawlerDetection do describe "crawler?" do + + it "can be amended via site settings" do + SiteSetting.crawler_user_agents = 'Mooble|Kaboodle+*' + expect(CrawlerDetection.crawler?("Mozilla/5.0 (compatible; Kaboodle+*/2.1; +http://www.google.com/bot.html)")).to eq(true) + expect(CrawlerDetection.crawler?("Mozilla/5.0 (compatible; Mooble+*/2.1; +http://www.google.com/bot.html)")).to eq(true) + expect(CrawlerDetection.crawler?("Mozilla/5.0 (compatible; Gooble+*/2.1; +http://www.google.com/bot.html)")).to eq(false) + end + it "returns true for crawler user agents" do # https://support.google.com/webmasters/answer/1061943?hl=en expect(described_class.crawler?("Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")).to eq(true) From d64853dfa09fbda202b8d335f023cb57dc52d00d Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 28 Sep 2017 22:04:19 +0530 Subject: [PATCH 008/108] FIX: update group.has_messages field weekly --- app/models/group.rb | 13 +++++++++++++ spec/models/group_spec.rb | 17 +++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/app/models/group.rb b/app/models/group.rb index 326d89781a..d569c2e289 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -308,6 +308,7 @@ class Group < ActiveRecord::Base def self.ensure_consistency! reset_all_counters! refresh_automatic_groups! + refresh_has_messages! end def self.reset_all_counters! @@ -331,6 +332,18 @@ class Group < ActiveRecord::Base args.each { |group| refresh_automatic_group!(group) } end + def self.refresh_has_messages! + exec_sql <<-SQL + UPDATE groups g SET has_messages = false + WHERE NOT EXISTS (SELECT tg.id + FROM topic_allowed_groups tg + INNER JOIN topics t ON t.id = tg.topic_id + WHERE tg.group_id = g.id + AND t.deleted_at IS NULL) + AND g.has_messages = true + SQL + end + def self.ensure_automatic_groups! AUTO_GROUPS.each_key do |name| refresh_automatic_group!(name) unless lookup_group(name) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 9eee947198..3920a7da43 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -577,4 +577,21 @@ describe Group do expect(group.group_users.map(&:user_id)).to contain_exactly(user.id, admin.id) end end + + it "Correctly updates has_messages" do + group = Fabricate(:group, has_messages: true) + topic = Fabricate(:private_message_topic) + + # when group message is not present + Group.refresh_has_messages! + group.reload + expect(group.has_messages?).to eq false + + # when group message is present + group.update!(has_messages: true) + TopicAllowedGroup.create!(topic_id: topic.id, group_id: group.id) + Group.refresh_has_messages! + group.reload + expect(group.has_messages?).to eq true + end end From 0358931b9f0a7f66188de34f29ba64ffa405f717 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 29 Sep 2017 12:58:15 +1000 Subject: [PATCH 009/108] correct erratic spec --- spec/jobs/publish_topic_to_category_spec.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/jobs/publish_topic_to_category_spec.rb b/spec/jobs/publish_topic_to_category_spec.rb index 2090b3106f..ebb5e79663 100644 --- a/spec/jobs/publish_topic_to_category_spec.rb +++ b/spec/jobs/publish_topic_to_category_spec.rb @@ -50,7 +50,9 @@ RSpec.describe Jobs::PublishTopicToCategory do message = MessageBus.track_publish do described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) - end.first + end.find do |m| + Hash === m.data && m.data.key?(:reload_topic) + end topic.reload expect(topic.category).to eq(another_category) From 41261b32a5099b7e2b87bdc96351cf067d09819a Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 29 Sep 2017 16:57:56 +1000 Subject: [PATCH 010/108] FIX: update message bus - Corrects broken short polling - Corrects after fork --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 061142dabf..dbdc540391 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM mail (2.6.6) mime-types (>= 1.16, < 4) memory_profiler (0.9.8) - message_bus (2.0.5) + message_bus (2.0.6) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) From f6c484881b01515200539b722646215c42848144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 29 Sep 2017 13:09:48 +0200 Subject: [PATCH 011/108] FIX: wasn't able to save watched/tracked/muted categories/tags --- app/controllers/users_controller.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index e0b850faa0..87f6332ab5 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -864,12 +864,12 @@ class UsersController < ApplicationController :website, :dismissed_banner_key, :profile_background, - :card_background, - :muted_category_ids, - :watched_category_ids, - :tracked_category_ids, - :watched_first_post_category_ids - ] + UserUpdater::OPTION_ATTR + :card_background + ] + + permitted.concat UserUpdater::OPTION_ATTR + permitted.concat UserUpdater::CATEGORY_IDS.keys.map { |k| { k => [] } } + permitted.concat UserUpdater::TAG_NAMES.keys.map { |k| { k => [] } } result = params .permit(permitted) From 0caf6a0f7d236a671c5c517422b55fa07ae9f243 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 29 Sep 2017 09:55:53 -0400 Subject: [PATCH 012/108] Support for HTML values in widget hbs templates --- .../discourse/components/discourse-topic.js.es6 | 1 - .../javascripts/discourse/widgets/raw-html.js.es6 | 7 ++++++- lib/javascripts/widget-hbs-compiler.js.es6 | 12 ++++++++++-- script/test_hbs_compiler.rb | 2 ++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 b/app/assets/javascripts/discourse/components/discourse-topic.js.es6 index ba81a63bf3..809d3b6f90 100644 --- a/app/assets/javascripts/discourse/components/discourse-topic.js.es6 +++ b/app/assets/javascripts/discourse/components/discourse-topic.js.es6 @@ -99,7 +99,6 @@ export default Ember.Component.extend(AddArchetypeClass, Scrolling, { // this happens after route exit, stuff could have trickled in this.appEvents.trigger('header:hide-topic'); this.appEvents.off('post:highlight'); - }, @observes('Discourse.hasFocus') diff --git a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 index e009d3caf4..740af14d2a 100644 --- a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 +++ b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 @@ -4,7 +4,7 @@ export default class RawHtml { } init() { - const $html = $(this.html); + const $html = $.parseHTML(this.html); this.decorate($html); return $html[0]; } @@ -20,3 +20,8 @@ export default class RawHtml { } RawHtml.prototype.type = 'Widget'; + +// TODO: Improve how helpers are registered for vdom compliation +if (typeof Discourse !== "undefined") { + Discourse.__widget_helpers.rawHtml = RawHtml; +} diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index 07d418ff2b..0f84f1c03d 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -64,7 +64,12 @@ function mustacheValue(node, state) { return `__iN("${icon}")`; break; default: - return `${resolve(path)}`; + if (node.escaped) { + return `${resolve(path)}`; + } else { + state.helpersUsed.rawHtml = true; + return `new __rH({ html: '' + ${resolve(path)} + ''})`; + } break; } } @@ -180,7 +185,10 @@ function compile(template) { let imports = ''; if (compiler.state.helpersUsed.iconNode) { - imports = "var __iN = Discourse.__widget_helpers.iconNode; "; + imports += "var __iN = Discourse.__widget_helpers.iconNode; "; + } + if (compiler.state.helpersUsed.rawHtml) { + imports += "var __rH = Discourse.__widget_helpers.rawHtml; "; } return `function(attrs, state) { ${imports}var _r = [];\n${code}\nreturn _r; }`; diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb index a6ef433592..d18a41fc17 100644 --- a/script/test_hbs_compiler.rb +++ b/script/test_hbs_compiler.rb @@ -1,5 +1,7 @@ template = <<~HBS {{attach widget="widget-name" attrs=attrs}} + {{a}} + {{{htmlValue}}} {{#if state.category}} {{attach widget="category-display" attrs=(hash category=state.category)}} {{/if}} From c1f174f554dbb524bf2c3337a8c3277ffbc834be Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 29 Sep 2017 10:19:35 -0400 Subject: [PATCH 013/108] FIX: Okay, try going back to the old way. Too many exceptions. --- app/assets/javascripts/discourse/widgets/raw-html.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 index 740af14d2a..c532946207 100644 --- a/app/assets/javascripts/discourse/widgets/raw-html.js.es6 +++ b/app/assets/javascripts/discourse/widgets/raw-html.js.es6 @@ -4,7 +4,7 @@ export default class RawHtml { } init() { - const $html = $.parseHTML(this.html); + const $html = $(this.html); this.decorate($html); return $html[0]; } From a370d7c7fd6d4bb3cff9f59a198c8625c266fbeb Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 29 Sep 2017 14:02:58 +0800 Subject: [PATCH 014/108] FIX: Compatibility between Client and Server routing. mend --- .../javascripts/discourse/models/topic.js.es6 | 2 ++ .../discourse/routes/app-route-map.js.es6 | 3 ++- .../routes/topic-by-slug-or-id.js.es6 | 16 +++++++++++++++ .../discourse/routes/topic-by-slug.js.es6 | 12 ----------- .../javascripts/discourse/routes/topic.js.es6 | 5 +++++ test/javascripts/acceptance/topic-test.js.es6 | 20 +++++++++++++++++++ .../helpers/create-pretender.js.es6 | 1 + 7 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 delete mode 100644 app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index fb3e16b888..3727290485 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -29,6 +29,8 @@ export function loadTopicView(topic, args) { }); } +export const ID_CONSTRAINT = /^\d+$/; + const Topic = RestModel.extend({ message: null, errorLoading: false, diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 7b6e59a895..8ec183d8b5 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -9,7 +9,8 @@ export default function() { this.route('fromParams', { path: '/' }); this.route('fromParamsNear', { path: '/:nearPost' }); }); - this.route('topicBySlug', { path: '/t/:slug', resetNamespace: true }); + + this.route('topicBySlugOrId', { path: '/t/:slugOrId', resetNamespace: true }); this.route('topicUnsubscribe', { path: '/t/:slug/:id/unsubscribe' }); this.route('discovery', { path: '/', resetNamespace: true }, function() { diff --git a/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 b/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 new file mode 100644 index 0000000000..6fcc92151a --- /dev/null +++ b/app/assets/javascripts/discourse/routes/topic-by-slug-or-id.js.es6 @@ -0,0 +1,16 @@ +import { default as Topic, ID_CONSTRAINT } from 'discourse/models/topic'; +import DiscourseURL from 'discourse/lib/url'; + +export default Discourse.Route.extend({ + model(params) { + if (params.slugOrId.match(ID_CONSTRAINT)) { + return { url: `/t/topic/${params.slugOrId}` }; + } else { + return Topic.idForSlug(params.slugOrId); + } + }, + + afterModel(result) { + DiscourseURL.routeTo(result.url, { replaceURL: true }); + } +}); diff --git a/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 b/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 deleted file mode 100644 index fc402e43c9..0000000000 --- a/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -import Topic from 'discourse/models/topic'; -import DiscourseURL from 'discourse/lib/url'; - -export default Discourse.Route.extend({ - model: function(params) { - return Topic.idForSlug(params.slug); - }, - - afterModel: function(result) { - DiscourseURL.routeTo(result.url, { replaceURL: true }); - } -}); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 7fc7fd098f..4e17be6c6b 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -1,4 +1,5 @@ import DiscourseURL from 'discourse/lib/url'; +import { ID_CONSTRAINT } from 'discourse/models/topic'; let isTransitioning = false, scheduledReplace = null, @@ -157,6 +158,10 @@ const TopicRoute = Discourse.Route.extend({ }, model(params, transition) { + if (params.slug.match(ID_CONSTRAINT)) { + return DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, { replaceURL: true }); + }; + const queryParams = transition.queryParams; let topic = this.modelFor('topic'); diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6 index a6510a2f47..530ea1cad3 100644 --- a/test/javascripts/acceptance/topic-test.js.es6 +++ b/test/javascripts/acceptance/topic-test.js.es6 @@ -142,6 +142,26 @@ QUnit.test("Reply as new message", assert => { }); }); +QUnit.test("Visit topic routes", assert => { + visit("/t/12"); + + andThen(() => { + assert.equal( + find('.fancy-title').text().trim(), 'PM for testing', + 'it routes to the right topic' + ); + }); + + visit("/t/280/20"); + + andThen(() => { + assert.equal( + find('.fancy-title').text().trim(), 'Internationalization / localization', + 'it routes to the right topic' + ); + }); +}); + QUnit.test("Updating the topic title with emojis", assert => { visit("/t/internationalization-localization/280"); click('#topic-title .d-icon-pencil'); diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index 6c70dad01f..a466f632cc 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -131,6 +131,7 @@ export default function() { this.put('/u/eviltrout.json', () => response({ user: {} })); this.get("/t/280.json", () => response(fixturesByUrl['/t/280/1.json'])); + this.get("/t/280/20.json", () => response(fixturesByUrl['/t/280/1.json'])); this.get("/t/28830.json", () => response(fixturesByUrl['/t/28830/1.json'])); this.get("/t/9.json", () => response(fixturesByUrl['/t/9/1.json'])); this.get("/t/12.json", () => response(fixturesByUrl['/t/12/1.json'])); From 00b190af75144b531de26dac6a59f8e54167aef3 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 29 Sep 2017 11:04:05 -0400 Subject: [PATCH 015/108] Revert "A safe way to create class variables in a multisite environment." The approach taken by this interface was flawed. We need a better solution. --- app/helpers/application_helper.rb | 5 +- app/models/group.rb | 5 +- app/models/post.rb | 5 +- app/models/topic_list.rb | 23 +++++--- lib/multisite_class_var.rb | 20 ------- lib/topic_view.rb | 6 +- spec/multisite/multisite_class_var_spec.rb | 67 ---------------------- 7 files changed, 24 insertions(+), 107 deletions(-) delete mode 100644 lib/multisite_class_var.rb delete mode 100644 spec/multisite/multisite_class_var_spec.rb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 8e81cfe5d5..3f57068e7a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -8,7 +8,6 @@ require_dependency 'mobile_detection' require_dependency 'category_badge' require_dependency 'global_path' require_dependency 'emoji' -require_dependency 'multisite_class_var' module ApplicationHelper include CurrentUser @@ -17,7 +16,9 @@ module ApplicationHelper include GlobalPath include MultisiteClassVar - multisite_class_var(:extra_body_classes) { Set.new } + def self.extra_body_classes + @extra_body_classes ||= Set.new + end def google_universal_analytics_json(ua_domain_name = nil) result = {} diff --git a/app/models/group.rb b/app/models/group.rb index d569c2e289..4083346854 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true require_dependency 'enum' -require_dependency 'multisite_class_var' class Group < ActiveRecord::Base include HasCustomFields include AnonCacheInvalidator - include MultisiteClassVar - multisite_class_var(:preloaded_custom_field_names) { Set.new } + cattr_accessor :preloaded_custom_field_names + self.preloaded_custom_field_names = Set.new has_many :category_groups, dependent: :destroy has_many :group_users, dependent: :destroy diff --git a/app/models/post.rb b/app/models/post.rb index 1aba30f24a..992e3a83a9 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -6,7 +6,6 @@ require_dependency 'post_analyzer' require_dependency 'validators/post_validator' require_dependency 'plugin/filter' require_dependency 'email_cook' -require_dependency 'multisite_class_var' require 'archetype' require 'digest/sha1' @@ -17,9 +16,9 @@ class Post < ActiveRecord::Base include Searchable include HasCustomFields include LimitedEdit - include MultisiteClassVar - multisite_class_var(:permitted_create_params) { Set.new } + cattr_accessor :permitted_create_params + self.permitted_create_params = Set.new # increase this number to force a system wide post rebake BAKED_VERSION = 1 diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index 3a3b005dfe..375659dc5c 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -1,24 +1,29 @@ require_dependency 'avatar_lookup' require_dependency 'primary_group_lookup' -require_dependency 'multisite_class_var' class TopicList include ActiveModel::Serialization - include MultisiteClassVar - multisite_class_var(:preloaded_custom_fields) { Set.new } - multisite_class_var(:preload_callbacks) { Set.new } + cattr_accessor :preloaded_custom_fields + self.preloaded_custom_fields = Set.new def self.on_preload(&blk) - preload_callbacks << blk + (@preload ||= Set.new) << blk end def self.cancel_preload(&blk) - preload_callbacks.delete(blk) + if @preload + @preload.delete blk + if @preload.length == 0 + @preload = nil + end + end end def self.preload(topics, object) - preload_callbacks.each { |p| p.call(topics, object) } + if @preload + @preload.each { |preload| preload.call(topics, object) } + end end attr_accessor :more_topics_url, @@ -112,8 +117,8 @@ class TopicList ft.topic_list = self end - if self.class.preloaded_custom_fields.present? - Topic.preload_custom_fields(@topics, self.class.preloaded_custom_fields) + if preloaded_custom_fields.present? + Topic.preload_custom_fields(@topics, preloaded_custom_fields) end TopicList.preload(@topics, self) diff --git a/lib/multisite_class_var.rb b/lib/multisite_class_var.rb deleted file mode 100644 index b21bca7d32..0000000000 --- a/lib/multisite_class_var.rb +++ /dev/null @@ -1,20 +0,0 @@ -# Support for a class variable that is multisite aware. - -module MultisiteClassVar - - def self.included(base) - base.extend ClassMethods - end - - module ClassMethods - def multisite_class_var(name, &default) - @multisite_class_vars ||= {} - @multisite_class_vars[name] = {} - - define_singleton_method(name) do - @multisite_class_vars[name][RailsMultisite::ConnectionManagement.current_db] ||= default.call - end - end - end - -end diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 2127f3b5f7..57626225eb 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -2,10 +2,8 @@ require_dependency 'guardian' require_dependency 'topic_query' require_dependency 'filter_best_posts' require_dependency 'gaps' -require_dependency 'multisite_class_var' class TopicView - include MultisiteClassVar attr_reader :topic, :posts, :guardian, :filtered_posts, :chunk_size, :print, :message_bus_last_id attr_accessor :draft, :draft_key, :draft_sequence, :user_custom_fields, :post_custom_fields @@ -26,7 +24,9 @@ class TopicView @default_post_custom_fields ||= ["action_code_who"] end - multisite_class_var(:post_custom_fields_whitelisters) { Set.new } + def self.post_custom_fields_whitelisters + @post_custom_fields_whitelisters ||= Set.new + end def self.add_post_custom_fields_whitelister(&block) post_custom_fields_whitelisters << block diff --git a/spec/multisite/multisite_class_var_spec.rb b/spec/multisite/multisite_class_var_spec.rb deleted file mode 100644 index 62cac14ffc..0000000000 --- a/spec/multisite/multisite_class_var_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'rails_helper' -require 'multisite_class_var' - -RSpec.describe MultisiteClassVar do - - it "will add the class variables" do - class_with_set = Class.new do - include MultisiteClassVar - multisite_class_var(:class_set) { Set.new } - multisite_class_var(:class_array) { Array.new } - end - - class_with_set.class_set << 'a' - class_with_set.class_array << 'c' - - expect(class_with_set.class_set).to contain_exactly('a') - expect(class_with_set.class_array).to contain_exactly('c') - end - - context "multisite environment" do - let(:conn) { RailsMultisite::ConnectionManagement } - - before do - @original_provider = SiteSetting.provider - SiteSetting.provider = SiteSettings::DbProvider.new(SiteSetting) - conn.config_filename = "spec/fixtures/multisite/two_dbs.yml" - conn.load_settings! - conn.remove_class_variable(:@@current_db) - end - - after do - conn.clear_settings! - [:@@db_spec_cache, :@@host_spec_cache, :@@default_spec].each do |class_variable| - conn.remove_class_variable(class_variable) - end - conn.set_current_db - SiteSetting.provider = @original_provider - end - - it "keeps the variable specific to the current site" do - class_with_set = Class.new do - include MultisiteClassVar - multisite_class_var(:class_set) { Set.new } - end - - conn.with_connection('default') do - expect(class_with_set.class_set).to be_blank - class_with_set.class_set << 'item0' - end - - conn.with_connection('second') do - expect(class_with_set.class_set).to be_blank - class_with_set.class_set << 'item1' - end - - conn.with_connection('default') do - expect(class_with_set.class_set).to contain_exactly('item0') - end - - conn.with_connection('second') do - expect(class_with_set.class_set).to contain_exactly('item1') - end - - end - end - -end From 2c2fe7eee4f5e629d0eb7e7b39a3ae004e627343 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 29 Sep 2017 11:09:25 -0400 Subject: [PATCH 016/108] FIX: Remove unused mixin --- app/helpers/application_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3f57068e7a..e7d1e7864e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -14,7 +14,6 @@ module ApplicationHelper include CanonicalURL::Helpers include ConfigurableUrls include GlobalPath - include MultisiteClassVar def self.extra_body_classes @extra_body_classes ||= Set.new From df0959953191249b1efaeb30dc664d2d06f989ee Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 29 Sep 2017 22:47:03 +0530 Subject: [PATCH 017/108] FIX: use different method name for topic rake task https://kevinjalbert.com/defined_methods-in-rake-tasks-you-re-gonna-have-a-bad-time/ cc @gschlager --- lib/tasks/topics.rake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/tasks/topics.rake b/lib/tasks/topics.rake index f0cb196f0e..bbbf56ac79 100644 --- a/lib/tasks/topics.rake +++ b/lib/tasks/topics.rake @@ -1,4 +1,4 @@ -def print_status(label, current, max) +def print_status_with_label(label, current, max) print "\r%s%9d / %d (%5.1f%%)" % [label, current, max, ((current.to_f / max.to_f) * 100).round(1)] end @@ -21,7 +21,7 @@ def close_old_topics(category) topics.find_each do |topic| topic.update_status("closed", true, Discourse.system_user) - print_status(" closing old topics: ", topics_closed += 1, total) + print_status_with_label(" closing old topics: ", topics_closed += 1, total) end end @@ -47,7 +47,7 @@ def apply_auto_close(category) topics.find_each do |topic| topic.inherit_auto_close_from_category - print_status(" applying auto-close to topics: ", topics_closed += 1, total) + print_status_with_label(" applying auto-close to topics: ", topics_closed += 1, total) end end From d5d66e969e9321942f3bd6666d604acc30b36c3b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 29 Sep 2017 14:02:34 -0400 Subject: [PATCH 018/108] FIX: js error when logging in using another Discourse site as sso provider --- app/assets/javascripts/discourse/controllers/login.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index a42f4233c4..c524939435 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -71,7 +71,7 @@ export default Ember.Controller.extend(ModalFunctionality, { type: 'POST' }).then(function (result) { // Successful login - if (result.error) { + if (result && result.error) { self.set('loggingIn', false); if (result.reason === 'not_activated') { self.send('showNotActivated', { From 4eeb6014f47f50d6e9cab3ee556f32769b9ee001 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Sat, 30 Sep 2017 09:09:12 +0800 Subject: [PATCH 019/108] Don't raise an error if user has been destroyed. --- app/jobs/regular/create_avatar_thumbnails.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/create_avatar_thumbnails.rb b/app/jobs/regular/create_avatar_thumbnails.rb index 046f50bfe1..31942f6c6d 100644 --- a/app/jobs/regular/create_avatar_thumbnails.rb +++ b/app/jobs/regular/create_avatar_thumbnails.rb @@ -9,7 +9,7 @@ module Jobs raise Discourse::InvalidParameters.new(:upload_id) if upload_id.blank? return unless upload = Upload.find_by(id: upload_id) - return unless user = User.find(args[:user_id] || upload.user_id) + return unless user = User.find_by(args[:user_id] || upload.user_id) Discourse.avatar_sizes.each do |size| OptimizedImage.create_for(upload, size, size, filename: upload.original_filename, allow_animation: SiteSetting.allow_animated_avatars) From ac04f5e0cccf493844e1eb855565d5de857ebcab Mon Sep 17 00:00:00 2001 From: Eleanor Demis Date: Sat, 30 Sep 2017 07:31:32 -0700 Subject: [PATCH 020/108] update response error when deleting tags (#5213) --- app/controllers/tags_controller.rb | 5 ++++- spec/controllers/tags_controller_spec.rb | 26 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 75ce09efa7..54115945e0 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -100,8 +100,11 @@ class TagsController < ::ApplicationController def destroy guardian.ensure_can_admin_tags! tag_name = params[:tag_id] + tag = Tag.find_by_name(tag_name) + raise Discourse::NotFound if tag.nil? + TopicCustomField.transaction do - Tag.find_by_name(tag_name).destroy + tag.destroy StaffActionLogger.new(current_user).log_custom('deleted_tag', subject: tag_name) end render json: success_json diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index f0e296345d..62c8c55d5a 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -190,4 +190,30 @@ describe TagsController do end end end + + describe 'destroy' do + context 'tagging enabled' do + before do + log_in(:admin) + SiteSetting.tagging_enabled = true + end + + context 'with an existent tag name' do + it 'deletes the tag' do + tag = Fabricate(:tag) + delete :destroy, params: { tag_id: tag.name }, format: :json + expect(response).to be_success + end + end + + context 'with a nonexistent tag name' do + it 'returns a tag not found message' do + delete :destroy, params: { tag_id: 'idontexist' }, format: :json + expect(response).not_to be_success + json = ::JSON.parse(response.body) + expect(json['error_type']).to eq('not_found') + end + end + end + end end From 8f7062bd7b2726042bbadf2623ccdca04eab535b Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 2 Oct 2017 10:59:55 +1100 Subject: [PATCH 021/108] FEATURE: reduce API key permission to TL0 --- config/site_settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index a145999d67..0331d5a520 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1435,7 +1435,7 @@ user_api: default: '' hidden: true min_trust_level_for_user_api_key: - default: 1 + default: 0 enum: 'TrustLevelSetting' allowed_user_api_push_urls: default: '' From 77ea063751289a6dc05977666c881fa39a8851a1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 10:24:37 +0800 Subject: [PATCH 022/108] FIX: Missing attribute. --- app/jobs/regular/create_avatar_thumbnails.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/create_avatar_thumbnails.rb b/app/jobs/regular/create_avatar_thumbnails.rb index 31942f6c6d..ef081cee95 100644 --- a/app/jobs/regular/create_avatar_thumbnails.rb +++ b/app/jobs/regular/create_avatar_thumbnails.rb @@ -9,7 +9,7 @@ module Jobs raise Discourse::InvalidParameters.new(:upload_id) if upload_id.blank? return unless upload = Upload.find_by(id: upload_id) - return unless user = User.find_by(args[:user_id] || upload.user_id) + return unless user = User.find_by(id: args[:user_id] || upload.user_id) Discourse.avatar_sizes.each do |size| OptimizedImage.create_for(upload, size, size, filename: upload.original_filename, allow_animation: SiteSetting.allow_animated_avatars) From 4e07bbfbbf02ea65682ca4b39e6b6a95a10c2caf Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 10:45:54 +0800 Subject: [PATCH 023/108] FIX: Only allow intergers for page params. --- app/controllers/list_controller.rb | 2 ++ spec/requests/list_controller_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 1a77a589e4..02b3cdd3e5 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -331,6 +331,8 @@ class ListController < ApplicationController def build_topic_list_options options = {} + params[:page] = params[:page].to_i rescue 1 + TopicQuery.public_valid_options.each do |key| options[key] = params[key] end diff --git a/spec/requests/list_controller_spec.rb b/spec/requests/list_controller_spec.rb index cec635654e..9a0d21a5c9 100644 --- a/spec/requests/list_controller_spec.rb +++ b/spec/requests/list_controller_spec.rb @@ -3,6 +3,18 @@ require 'rails_helper' RSpec.describe ListController do let(:topic) { Fabricate(:topic) } + describe '#index' do + it "doesn't throw an error with a negative page" do + get "/#{Discourse.anonymous_filters[1]}", params: { page: -1024 } + expect(response).to be_success + end + + it "doesn't throw an error with page params as an array" do + get "/#{Discourse.anonymous_filters[1]}", params: { page: ['7'] } + expect(response).to be_success + end + end + describe 'titles for crawler layout' do it 'has no title for the default URL' do topic From 049d92521321cdb9e7b8059c97344bd7364282ea Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 10:47:22 +0800 Subject: [PATCH 024/108] Remove controller spec that is rewritten as request spec. --- spec/controllers/list_controller_spec.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb index 3de5a762b2..412833aa17 100644 --- a/spec/controllers/list_controller_spec.rb +++ b/spec/controllers/list_controller_spec.rb @@ -28,11 +28,6 @@ describe ListController do parsed = JSON.parse(response.body) expect(parsed["topic_list"]["topics"].length).to eq(1) end - - it "doesn't throw an error with a negative page" do - get :top, params: { page: -1024 } - expect(response).to be_success - end end describe 'RSS feeds' do From b295a399773eac505b8d613dafd1f5ba5c575198 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 11:24:48 +0800 Subject: [PATCH 025/108] Fix randomly failing spec. --- spec/controllers/uploads_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 58819817aa..480da63c50 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -164,7 +164,7 @@ describe UploadsController do for_private_message: "true", format: :json } - end.first + end.find { |m| m.channel = '/uploads/composer' } expect(response).to be_success expect(message.data["id"]).to be From c872225762a7b7b8f3b0baf1c2de35ed171e0658 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 11:34:57 +0800 Subject: [PATCH 026/108] Improve `MessageBus.track_publish` to allow filter by channel. --- spec/components/post_merger_spec.rb | 3 +-- spec/components/post_revisor_spec.rb | 5 ++-- .../components/site_setting_extension_spec.rb | 6 ++--- spec/controllers/uploads_controller_spec.rb | 25 ++++++++----------- spec/models/user_spec.rb | 7 +++--- spec/requests/admin/emojis_controller_spec.rb | 16 +++++------- spec/support/diagnostics_helper.rb | 5 ++-- 7 files changed, 28 insertions(+), 39 deletions(-) diff --git a/spec/components/post_merger_spec.rb b/spec/components/post_merger_spec.rb index c620a8ea8b..250712968a 100644 --- a/spec/components/post_merger_spec.rb +++ b/spec/components/post_merger_spec.rb @@ -15,9 +15,8 @@ describe PostMerger do reply3 = create_post(topic: topic, raw: 'The third reply', post_number: 4, user: user) replies = [reply3, reply2, reply1] - message = MessageBus.track_publish { PostMerger.new(admin, replies).merge }.last + message = MessageBus.track_publish("/topic/#{topic.id}") { PostMerger.new(admin, replies).merge }.last - expect(message.channel).to eq("/topic/#{topic.id}") expect(message.data[:type]).to eq(:revised) expect(message.data[:post_number]).to eq(reply3.post_number) diff --git a/spec/components/post_revisor_spec.rb b/spec/components/post_revisor_spec.rb index c057daaf2e..7f0f950add 100644 --- a/spec/components/post_revisor_spec.rb +++ b/spec/components/post_revisor_spec.rb @@ -382,11 +382,10 @@ describe PostRevisor do it "should publish topic changes to clients" do revisor = described_class.new(topic.ordered_posts.first, topic) - messages = MessageBus.track_publish do + message = MessageBus.track_publish("/topic/#{topic.id}") do revisor.revise!(newuser, title: 'this is a test topic') - end + end.first - message = messages.find { |m| m.channel == "/topic/#{topic.id}" } payload = message.data expect(payload[:reload_topic]).to eq(true) end diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 5d87e8f983..1c2d8cb7f9 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -145,11 +145,11 @@ describe SiteSettingExtension do settings.setting("test_setting", 100) settings.setting("test_setting", nil, client: true) - messages = MessageBus.track_publish do + message = MessageBus.track_publish('/client_settings') do settings.test_setting = 88 - end + end.first - expect(messages.map(&:channel).include?('/client_settings')).to eq(true) + expect(message).to be_present end end end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 480da63c50..f77645e543 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -52,13 +52,11 @@ describe UploadsController do it 'is successful with an image' do Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything) - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/avatar') do post :create, params: { file: logo, type: "avatar", format: :json } - end.find { |m| m.channel == "/uploads/avatar" } + end.first expect(response.status).to eq 200 - - expect(message.channel).to eq("/uploads/avatar") expect(message.data["id"]).to be end @@ -67,12 +65,11 @@ describe UploadsController do Jobs.expects(:enqueue).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { file: text_file, type: "composer", format: :json } - end.find { |m| m.channel == "/uploads/composer" } + end.first expect(response.status).to eq 200 - expect(message.channel).to eq("/uploads/composer") expect(message.data["id"]).to be end @@ -103,7 +100,7 @@ describe UploadsController do log_in :admin Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/profile_background') do post :create, params: { file: logo, retain_hours: 100, @@ -119,7 +116,7 @@ describe UploadsController do it 'requires a file' do Jobs.expects(:enqueue).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { type: "composer", format: :json } end.first @@ -157,14 +154,14 @@ describe UploadsController do SiteSetting.allow_staff_to_upload_any_file_in_pm = true @user.update_columns(moderator: true) - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { file: text_file, type: "composer", for_private_message: "true", format: :json } - end.find { |m| m.channel = '/uploads/composer' } + end.first expect(response).to be_success expect(message.data["id"]).to be @@ -173,13 +170,11 @@ describe UploadsController do it 'returns an error when it could not determine the dimensions of an image' do Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never - message = MessageBus.track_publish do + message = MessageBus.track_publish('/uploads/composer') do post :create, params: { file: fake_jpg, type: "composer", format: :json } - end.find { |m| m.channel == '/uploads/composer' } + end.first expect(response.status).to eq 200 - - expect(message.channel).to eq("/uploads/composer") expect(message.data["errors"]).to contain_exactly(I18n.t("upload.images.size_not_found")) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 65461bfd24..cd07511834 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1420,9 +1420,8 @@ describe User do let(:user) { Fabricate(:user) } it 'should publish the right message' do - message = MessageBus.track_publish { user.logged_out }.find { |m| m.channel == '/logout' } + message = MessageBus.track_publish('/logout') { user.logged_out }.first - expect(message.channel).to eq('/logout') expect(message.data).to eq(user.id) end end @@ -1527,9 +1526,9 @@ describe User do notification = Fabricate(:notification, user: user) notification2 = Fabricate(:notification, user: user, read: true) - message = MessageBus.track_publish do + message = MessageBus.track_publish("/notification/#{user.id}") do user.publish_notifications_state - end.find { |m| m.channel = "/notification/#{user.id}" } + end.first expect(message.data[:recent]).to eq([ [notification2.id, true], [notification.id, false] diff --git a/spec/requests/admin/emojis_controller_spec.rb b/spec/requests/admin/emojis_controller_spec.rb index 329778e50e..d2d0250584 100644 --- a/spec/requests/admin/emojis_controller_spec.rb +++ b/spec/requests/admin/emojis_controller_spec.rb @@ -11,14 +11,13 @@ RSpec.describe Admin::EmojisController do describe "#create" do describe 'when upload is invalid' do it 'should publish the right error' do - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/emoji") do post "/admin/customize/emojis.json", params: { name: 'test', file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/fake.jpg") } - end.find { |m| m.channel == "/uploads/emoji" } + end.first - expect(message.channel).to eq("/uploads/emoji") expect(message.data["errors"]).to eq([I18n.t('upload.images.size_not_found')]) end end @@ -27,14 +26,12 @@ RSpec.describe Admin::EmojisController do it 'should publish the right error' do CustomEmoji.create!(name: 'test', upload: upload) - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/emoji") do post "/admin/customize/emojis.json", params: { name: 'test', file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") } - end.find { |m| m.channel == "/uploads/emoji" } - - expect(message.channel).to eq("/uploads/emoji") + end.first expect(message.data["errors"]).to eq([ "Name #{I18n.t('activerecord.errors.models.custom_emoji.attributes.name.taken')}" @@ -45,18 +42,17 @@ RSpec.describe Admin::EmojisController do it 'should allow an admin to add a custom emoji' do Emoji.expects(:clear_cache) - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/emoji") do post "/admin/customize/emojis.json", params: { name: 'test', file: fixture_file_upload("#{Rails.root}/spec/fixtures/images/logo.png") } - end.find { |m| m.channel == "/uploads/emoji" } + end.first custom_emoji = CustomEmoji.last upload = custom_emoji.upload expect(upload.original_filename).to eq('logo.png') - expect(message.channel).to eq("/uploads/emoji") expect(message.data["errors"]).to eq(nil) expect(message.data["name"]).to eq(custom_emoji.name) expect(message.data["url"]).to eq(upload.url) diff --git a/spec/support/diagnostics_helper.rb b/spec/support/diagnostics_helper.rb index bab725d817..11d131c98d 100644 --- a/spec/support/diagnostics_helper.rb +++ b/spec/support/diagnostics_helper.rb @@ -1,7 +1,7 @@ module MessageBus::DiagnosticsHelper def publish(channel, data, opts = nil) id = super(channel, data, opts) - if @tracking + if @tracking && (@channel.nil? || @channel == channel) m = MessageBus::Message.new(-1, id, channel, data) m.user_ids = opts[:user_ids] if opts m.group_ids = opts[:group_ids] if opts @@ -10,7 +10,8 @@ module MessageBus::DiagnosticsHelper id end - def track_publish + def track_publish(channel = nil) + @channel = channel @tracking = tracking = [] yield tracking From 95358304d981aba9a260d4268699aca5d49192d2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 12:00:43 +0800 Subject: [PATCH 027/108] FIX: Don't raise an error when post has been destroyed. --- plugins/discourse-presence/plugin.rb | 70 ++++++++++--------- .../presence_controller_spec.rb | 38 +++++++--- 2 files changed, 65 insertions(+), 43 deletions(-) rename plugins/discourse-presence/spec/{ => requests}/presence_controller_spec.rb (77%) diff --git a/plugins/discourse-presence/plugin.rb b/plugins/discourse-presence/plugin.rb index 7d7a57ac8f..a5e54fa88f 100644 --- a/plugins/discourse-presence/plugin.rb +++ b/plugins/discourse-presence/plugin.rb @@ -100,71 +100,75 @@ after_initialize do before_action :ensure_logged_in def publish - data = params.permit(:response_needed, - current: [:action, :topic_id, :post_id], - previous: [:action, :topic_id, :post_id] - ) + data = params.permit( + :response_needed, + current: [:action, :topic_id, :post_id], + previous: [:action, :topic_id, :post_id] + ) - if data[:previous] && - data[:previous][:action].in?(['edit', 'reply']) + payload = {} + if data[:previous] && data[:previous][:action].in?(['edit', 'reply']) type = data[:previous][:post_id] ? 'post' : 'topic' id = data[:previous][:post_id] ? data[:previous][:post_id] : data[:previous][:topic_id] topic = if type == 'post' - Post.find_by(id: id).topic + Post.find_by(id: id)&.topic else Topic.find_by(id: id) end - guardian.ensure_can_see!(topic) + if topic + guardian.ensure_can_see!(topic) - any_changes = false - any_changes ||= Presence::PresenceManager.remove(type, id, current_user.id) - any_changes ||= Presence::PresenceManager.cleanup(type, id) + any_changes = false + any_changes ||= Presence::PresenceManager.remove(type, id, current_user.id) + any_changes ||= Presence::PresenceManager.cleanup(type, id) - users = Presence::PresenceManager.publish(type, id) if any_changes + users = Presence::PresenceManager.publish(type, id) if any_changes + end end - if data[:current] && - data[:current][:action].in?(['edit', 'reply']) - + if data[:current] && data[:current][:action].in?(['edit', 'reply']) type = data[:current][:post_id] ? 'post' : 'topic' id = data[:current][:post_id] ? data[:current][:post_id] : data[:current][:topic_id] topic = if type == 'post' - Post.find_by!(id: id).topic + Post.find_by(id: id)&.topic else - Topic.find_by!(id: id) + Topic.find_by(id: id) end - guardian.ensure_can_see!(topic) + if topic + guardian.ensure_can_see!(topic) - any_changes = false - any_changes ||= Presence::PresenceManager.add(type, id, current_user.id) - any_changes ||= Presence::PresenceManager.cleanup(type, id) + any_changes = false + any_changes ||= Presence::PresenceManager.add(type, id, current_user.id) + any_changes ||= Presence::PresenceManager.cleanup(type, id) - users = Presence::PresenceManager.publish(type, id) if any_changes + users = Presence::PresenceManager.publish(type, id) if any_changes - if data[:response_needed] - users ||= Presence::PresenceManager.get_users(type, id) + if data[:response_needed] + users ||= Presence::PresenceManager.get_users(type, id) - serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } + serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } - messagebus_channel = Presence::PresenceManager.get_messagebus_channel(type, id) + messagebus_channel = Presence::PresenceManager.get_messagebus_channel(type, id) - render json: { - messagebus_channel: messagebus_channel, - messagebus_id: MessageBus.last_id(messagebus_channel), - users: serialized_users - } - return + { + messagebus_channel: messagebus_channel, + messagebus_id: MessageBus.last_id(messagebus_channel), + users: serialized_users + } + end + else + {} end end - render json: {} + render json: payload end end diff --git a/plugins/discourse-presence/spec/presence_controller_spec.rb b/plugins/discourse-presence/spec/requests/presence_controller_spec.rb similarity index 77% rename from plugins/discourse-presence/spec/presence_controller_spec.rb rename to plugins/discourse-presence/spec/requests/presence_controller_spec.rb index 28d62753b3..2e667e0c02 100644 --- a/plugins/discourse-presence/spec/presence_controller_spec.rb +++ b/plugins/discourse-presence/spec/requests/presence_controller_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' -describe ::Presence::PresencesController, type: :request do - +describe ::Presence::PresencesController do before do SiteSetting.presence_enabled = true end @@ -13,7 +12,7 @@ describe ::Presence::PresencesController, type: :request do let(:post1) { Fabricate(:post) } let(:post2) { Fabricate(:post) } - after(:each) do + after do $redis.del("presence:topic:#{post1.topic.id}") $redis.del("presence:topic:#{post2.topic.id}") $redis.del("presence:post:#{post1.id}") @@ -36,7 +35,6 @@ describe ::Presence::PresencesController, type: :request do end it "uses guardian to secure endpoint" do - # Private message private_post = Fabricate(:private_message_post) post '/presence/publish.json', params: { @@ -45,7 +43,6 @@ describe ::Presence::PresencesController, type: :request do expect(response.code.to_i).to eq(403) - # Secure category group = Fabricate(:group) category = Fabricate(:private_category, group: group) private_topic = Fabricate(:topic, category: category) @@ -64,7 +61,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (1) + expect(messages.count).to eq(1) data = JSON.parse(response.body) @@ -80,7 +77,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (1) + expect(messages.count).to eq(1) data = JSON.parse(response.body) expect(data).to eq({}) @@ -93,7 +90,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (1) + expect(messages.count).to eq(1) messages = MessageBus.track_publish do post '/presence/publish.json', params: { @@ -101,7 +98,7 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (0) + expect(messages.count).to eq(0) end it "clears 'previous' state when supplied" do @@ -116,7 +113,28 @@ describe ::Presence::PresencesController, type: :request do } end - expect(messages.count).to eq (3) + expect(messages.count).to eq(3) + end + + describe 'when post has been deleted' do + it 'should return an empty response' do + post1.destroy! + + post '/presence/publish.json', params: { + current: { compose_state: 'open', action: 'edit', post_id: post1.id } + } + + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to eq({}) + + post '/presence/publish.json', params: { + current: { compose_state: 'open', action: 'edit', post_id: post2.id }, + previous: { compose_state: 'open', action: 'edit', post_id: post1.id } + } + + expect(response.status).to eq(200) + expect(JSON.parse(response.body)).to eq({}) + end end end From 9fa575dca182f535fb57d5868d31f4b0c5fe9ec9 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 2 Oct 2017 15:21:45 +1100 Subject: [PATCH 028/108] Update message bus This corrects a rare race condition. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index dbdc540391..3b1aec0d30 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -150,7 +150,7 @@ GEM mail (2.6.6) mime-types (>= 1.16, < 4) memory_profiler (0.9.8) - message_bus (2.0.6) + message_bus (2.0.7) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) From 5c1d551e9c438739d0e9c85abaa54b3b4638995a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 12:25:38 +0800 Subject: [PATCH 029/108] Fix broken spec. --- plugins/discourse-presence/plugin.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/discourse-presence/plugin.rb b/plugins/discourse-presence/plugin.rb index a5e54fa88f..1383b5d245 100644 --- a/plugins/discourse-presence/plugin.rb +++ b/plugins/discourse-presence/plugin.rb @@ -157,14 +157,12 @@ after_initialize do messagebus_channel = Presence::PresenceManager.get_messagebus_channel(type, id) - { + payload = { messagebus_channel: messagebus_channel, messagebus_id: MessageBus.last_id(messagebus_channel), users: serialized_users } end - else - {} end end From 974836962d0cbd0821366eef4c7627ef60b160fa Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 12:50:22 +0800 Subject: [PATCH 030/108] Fix invalid method call. --- app/jobs/regular/emit_web_hook_event.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 341702904a..305c10d7e0 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -8,7 +8,7 @@ module Jobs end web_hook = WebHook.find_by(id: args[:web_hook_id]) - raise Discourse::InvalidParameters(:web_hook_id) if web_hook.blank? + raise Discourse::InvalidParameters.new(:web_hook_id) if web_hook.blank? unless ping_event?(args[:event_type]) return unless web_hook.active? From 0f2c5f5fc9ffe47f402cbaf3783e8b2378fb2762 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 12:58:27 +0800 Subject: [PATCH 031/108] FIX: Don't raise error when trying to download avatar from URL. --- app/jobs/regular/download_avatar_from_url.rb | 10 +++++++++- spec/jobs/download_avatar_from_url_spec.rb | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 spec/jobs/download_avatar_from_url_spec.rb diff --git a/app/jobs/regular/download_avatar_from_url.rb b/app/jobs/regular/download_avatar_from_url.rb index 83e687fed9..f4eabdf951 100644 --- a/app/jobs/regular/download_avatar_from_url.rb +++ b/app/jobs/regular/download_avatar_from_url.rb @@ -12,7 +12,15 @@ module Jobs return unless user = User.find_by(id: user_id) - UserAvatar.import_url_for_user(url, user, override_gravatar: args[:override_gravatar]) + begin + UserAvatar.import_url_for_user( + '/assets/vorablesen/placeholder-user-ed74bdf68223d030da1b7ddc44f59faf9c5a184388c94aff91632d5bf166a9e5.png', + user, + override_gravatar: args[:override_gravatar] + ) + rescue Discourse::InvalidParameters => e + raise e unless e.message == 'url' + end end end diff --git a/spec/jobs/download_avatar_from_url_spec.rb b/spec/jobs/download_avatar_from_url_spec.rb new file mode 100644 index 0000000000..6758a647a0 --- /dev/null +++ b/spec/jobs/download_avatar_from_url_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe Jobs::DownloadAvatarFromUrl do + let(:user) { Fabricate(:user) } + + describe 'when url is invalid' do + it 'should not raise any error' do + expect do + described_class.new.execute( + url: '/assets/something/nice.jpg', + user_id: user.id + ) + end.to_not raise_error + end + end +end From b5bbb8ae8a8fd8c91085639378b969414266c487 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 13:16:01 +0800 Subject: [PATCH 032/108] Fix failing spec. --- spec/components/discourse_redis_spec.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/components/discourse_redis_spec.rb b/spec/components/discourse_redis_spec.rb index 374ee3c161..07742fce54 100644 --- a/spec/components/discourse_redis_spec.rb +++ b/spec/components/discourse_redis_spec.rb @@ -184,10 +184,13 @@ describe DiscourseRedis do it 'should fallback to the master server once it is up' do fallback_handler.master = false - Redis::Client.any_instance.expects(:call).with([:info]).returns(DiscourseRedis::FallbackHandler::MASTER_LINK_STATUS) + redis_connection = DiscourseRedis.raw_connection.client + Redis::Client.expects(:new).with(DiscourseRedis.slave_config).returns(redis_connection) + + redis_connection.expects(:call).with([:info]).returns(DiscourseRedis::FallbackHandler::MASTER_LINK_STATUS) DiscourseRedis::FallbackHandler::CONNECTION_TYPES.each do |connection_type| - Redis::Client.any_instance.expects(:call).with([:client, [:kill, 'type', connection_type]]) + redis_connection.expects(:call).with([:client, [:kill, 'type', connection_type]]) end expect(fallback_handler.initiate_fallback_to_master).to eq(true) From ac666ddf17e6cada37e4a6e934bf3232fb4c7108 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Sun, 17 Sep 2017 00:50:32 -0400 Subject: [PATCH 033/108] PollFeed: check 'content:encoded' for content first --- app/jobs/scheduled/poll_feed.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/jobs/scheduled/poll_feed.rb b/app/jobs/scheduled/poll_feed.rb index fcd0243aae..798cbff789 100644 --- a/app/jobs/scheduled/poll_feed.rb +++ b/app/jobs/scheduled/poll_feed.rb @@ -101,7 +101,9 @@ module Jobs end def content - @article_rss_item.content.try(:force_encoding, "UTF-8").try(:scrub) || @article_rss_item.description.try(:force_encoding, "UTF-8").try(:scrub) + @article_rss_item.content_encoded&.force_encoding("UTF-8")&.scrub || + @article_rss_item.content&.force_encoding("UTF-8")&.scrub || + @article_rss_item.description&.force_encoding("UTF-8")&.scrub end def title From 15cd3b78aec7f4716b1a5c9e36686b415a2edf79 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Wed, 20 Sep 2017 21:14:39 -0400 Subject: [PATCH 034/108] integration test for PollFeed job --- spec/fixtures/feed/feed.rss | 30 ++++++++++++++++ spec/jobs/poll_feed_spec.rb | 69 +++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 spec/fixtures/feed/feed.rss diff --git a/spec/fixtures/feed/feed.rss b/spec/fixtures/feed/feed.rss new file mode 100644 index 0000000000..2de7185c43 --- /dev/null +++ b/spec/fixtures/feed/feed.rss @@ -0,0 +1,30 @@ + + + Discourse + + https://blog.discourse.org + Official blog for the open source Discourse project + Thu, 14 Sep 2017 15:22:33 +0000 + en-US + hourly + 1 + https://wordpress.org/?v=4.8.1 + + Poll Feed Spec Fixture + https://blog.discourse.org/2017/09/poll-feed-spec-fixture/ + Thu, 14 Sep 2017 15:22:33 +0000 + + + https://blog.discourse.org/?p=pollfeedspec + + This is the body & content.

]]>
+
+
+
diff --git a/spec/jobs/poll_feed_spec.rb b/spec/jobs/poll_feed_spec.rb index 0fcfa7d02c..27f108b3ee 100644 --- a/spec/jobs/poll_feed_spec.rb +++ b/spec/jobs/poll_feed_spec.rb @@ -43,4 +43,73 @@ describe Jobs::PollFeed do end + describe '#poll_feed' do + let(:embed_by_username) { 'eviltrout' } + let(:embed_username_key_from_feed) { 'dc_creator' } + let!(:default_user) { Fabricate(:evil_trout) } + let!(:feed_author) { Fabricate(:user, username: 'xrav3nz', email: 'hi@bye.com') } + + before do + SiteSetting.feed_polling_enabled = true + SiteSetting.feed_polling_url = 'https://blog.discourse.org/feed/' + SiteSetting.embed_by_username = embed_by_username + + stub_request(:get, SiteSetting.feed_polling_url).to_return( + status: 200, + body: file_from_fixtures('feed.rss', 'feed').read, + headers: { "Content-Type" => "application/rss+xml" } + ) + end + + describe 'author username parsing' do + context 'when neither embed_by_username nor embed_username_key_from_feed is set' do + before do + SiteSetting.embed_by_username = "" + SiteSetting.embed_username_key_from_feed = "" + end + + it 'does not import topics' do + expect { poller.poll_feed }.not_to change { Topic.count } + end + end + + context 'when embed_by_username is set' do + before do + SiteSetting.embed_by_username = embed_by_username + SiteSetting.embed_username_key_from_feed = "" + end + + it 'creates the new topics under embed_by_username' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.user).to eq(default_user) + end + end + + context 'when embed_username_key_from_feed is set' do + before do + SiteSetting.embed_username_key_from_feed = embed_username_key_from_feed + end + + it 'creates the new topics under the username found' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.user).to eq(feed_author) + end + end + end + + it 'parses the title correctly' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.title).to eq('Poll Feed Spec Fixture') + end + + it 'parses the content correctly' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.first_post.raw).to include('

This is the body & content.

') + end + + it 'parses the link correctly' do + expect { poller.poll_feed }.to change { Topic.count }.by(1) + expect(Topic.last.topic_embed.embed_url).to eq('https://blog.discourse.org/2017/09/poll-feed-spec-fixture') + end + end end From 79f3d299a160ecaa26504b95acbf2420398cb294 Mon Sep 17 00:00:00 2001 From: OsamaSayegh Date: Mon, 2 Oct 2017 11:04:58 +0300 Subject: [PATCH 035/108] Don't allow category definition topics to be converted to PMs (#5216) --- .../javascripts/discourse/widgets/topic-admin-menu.js.es6 | 2 +- app/serializers/topic_view_serializer.rb | 1 + lib/guardian/topic_guardian.rb | 2 ++ spec/components/guardian_spec.rb | 6 ++++++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 index 212d549d42..003de97cea 100644 --- a/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-admin-menu.js.es6 @@ -177,7 +177,7 @@ export default createWidget('topic-admin-menu', { icon: visible ? 'eye-slash' : 'eye', label: visible ? 'actions.invisible' : 'actions.visible' }); - if (this.currentUser.get('staff')) { + if (details.get('can_convert_topic')) { buttons.push({ className: 'topic-admin-convert', action: isPrivateMessage ? 'convertToPublicTopic' : 'convertToPrivateMessage', icon: isPrivateMessage ? 'comment' : 'envelope', diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index ab1068e4a5..c5ccc91789 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -118,6 +118,7 @@ class TopicViewSerializer < ApplicationSerializer result[:can_create_post] = true if scope.can_create?(Post, object.topic) result[:can_reply_as_new_topic] = true if scope.can_reply_as_new_topic?(object.topic) result[:can_flag_topic] = actions_summary.any? { |a| a[:can_act] } + result[:can_convert_topic] = true if scope.can_convert_topic?(object.topic) result end diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb index 8b8f08a12d..b8db72d315 100644 --- a/lib/guardian/topic_guardian.rb +++ b/lib/guardian/topic_guardian.rb @@ -63,7 +63,9 @@ module TopicGuardian end def can_convert_topic?(topic) + return false if topic.blank? return false if topic && topic.trashed? + return false if Category.where("topic_id = ?", topic.id).exists? return true if is_admin? is_moderator? && can_create_post?(topic) end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 9b43ac9765..82ee29609a 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -996,6 +996,12 @@ describe Guardian do expect(Guardian.new(trust_level_4).can_convert_topic?(topic)).to be_falsey end + it 'returns false for category definition topics' do + c = Fabricate(:category) + topic = Topic.find_by(id: c.topic_id) + expect(Guardian.new(admin).can_convert_topic?(topic)).to be_falsey + end + it 'returns true when a moderator' do expect(Guardian.new(moderator).can_convert_topic?(topic)).to be_truthy end From 4ae3a4e89edbd9ffe40fb03b069b9dfeb6038a7f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 2 Oct 2017 16:07:27 +0800 Subject: [PATCH 036/108] UX: Label should toggle checkbox. https://meta.discourse.org/t/clicking-label-for-automatically-set-as-primary-group-doesnt-toggle-setting/71086/2 --- app/assets/javascripts/admin/templates/group.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index 03be28e034..0689bd30ce 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -103,7 +103,7 @@ {{/if}}
-
diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index e01f05ebfb..fcc9d716f2 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -307,7 +307,9 @@
{{i18n-yes-no model.isSuspended}} {{#if model.isSuspended}} - {{i18n "admin.user.suspended_until" until=model.suspendedTillDate}} + {{#unless model.suspendedForever}} + {{i18n "admin.user.suspended_until" until=model.suspendedTillDate}} + {{/unless}} {{/if}}
diff --git a/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 b/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 index f4c9a00fc8..034ebb9154 100644 --- a/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 @@ -9,6 +9,8 @@ const LATER_THIS_WEEK = 'later_this_week'; const THIS_WEEKEND = 'this_weekend'; const NEXT_WEEK = 'next_week'; const NEXT_MONTH = 'next_month'; +const FOREVER = 'forever'; + export const PICK_DATE_AND_TIME = 'pick_date_and_time'; export const SET_BASED_ON_LAST_POST = 'set_based_on_last_post'; @@ -66,6 +68,13 @@ export default Combobox.extend({ }); } + if (this.get('includeForever')) { + selections.push({ + id: FOREVER, + name: I18n.t('topic.auto_update_input.forever') + }); + } + selections.push({ id: PICK_DATE_AND_TIME, name: I18n.t('topic.auto_update_input.pick_date_and_time') @@ -133,7 +142,7 @@ export default Combobox.extend({ output += `${state.text}`; - if (time) { + if (time && state.id !== FOREVER) { output += `${time}`; } @@ -170,6 +179,10 @@ export default Combobox.extend({ time = time.add(1, 'month').startOf('month').hour(timeOfDay).minute(0); icon = 'briefcase'; break; + case FOREVER: + time = time.add(1000, 'year').hour(timeOfDay).minute(0); + icon = 'gavel'; + break; case PICK_DATE_AND_TIME: time = null; icon = 'calendar-plus-o'; diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 93dce42015..10a822a4e9 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -178,6 +178,11 @@ const User = RestModel.extend({ return suspendedTill && moment(suspendedTill).isAfter(); }, + @computed("suspended_till") + suspendedForever(suspendedTill) { + return moment().diff(suspendedTill, 'years') < -500; + }, + @computed("suspended_till") suspendedTillDate(suspendedTill) { return longDate(suspendedTill); diff --git a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs index 3f5f984893..ed205d9a00 100644 --- a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs @@ -8,6 +8,7 @@ statusType=statusType value=selection input=input + includeForever=includeForever width="50%" none="topic.auto_update_input.none"}}
diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 7c654823ef..17677dd3d1 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -84,7 +84,13 @@ {{#if model.isSuspended}}
{{d-icon "ban"}} - {{i18n 'user.suspended_notice' date=model.suspendedTillDate}}
+ + {{#if model.suspendedForever}} + {{i18n 'user.suspended_permanently'}} + {{else}} + {{i18n 'user.suspended_notice' date=model.suspendedTillDate}} + {{/if}} +
{{#if model.suspend_reason}} {{i18n 'user.suspended_reason'}} {{model.suspend_reason}} {{/if}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 80ffb7603c..e3a3f91f44 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -604,6 +604,7 @@ en: admin_tooltip: "This user is an admin" blocked_tooltip: "This user is blocked" suspended_notice: "This user is suspended until {{date}}." + suspended_permanently: "This user is suspended." suspended_reason: "Reason: " github_profile: "Github" email_activity_summary: "Activity Summary" @@ -1568,6 +1569,7 @@ en: this_weekend: "This weekend" next_week: "Next week" next_month: "Next month" + forever: "Forever" pick_date_and_time: "Pick date and time" set_based_on_last_post: "Close based on last post" publish_to_category: From 56793d6853ff6200bafdaac1e6f82d7153819a54 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 2 Oct 2017 15:14:07 -0400 Subject: [PATCH 044/108] FIX: Better flagging CSS on mobile --- .../stylesheets/common/admin/flagging.scss | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index f16de2bd97..b2a416691b 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -194,21 +194,9 @@ .mobile-view { .flagged-posts { - .flagged-post-details { - flex-wrap: wrap; - justify-content: flex-start; - - .flagged-post-avatar { - margin-right: 10px; - } - - .flagged-post-excerpt { - width: 70%; - } - - .flaggers { - margin-left: 4em; - margin-bottom: 1em; + .flagged-post { + .flag-user-lists { + display: block; } } } From 1022535c2b5c0f942ebfc2230dfae820c5010b39 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 2 Oct 2017 15:23:58 -0400 Subject: [PATCH 045/108] UX: Add a two week suspension timeframe --- .../components/future-date-input-selector.js.es6 | 15 ++++++++++++--- .../templates/components/future-date-input.hbs | 1 + .../templates/modal/edit-topic-timer.hbs | 3 +++ config/locales/client.en.yml | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 b/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 index 034ebb9154..df1b2e871c 100644 --- a/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/future-date-input-selector.js.es6 @@ -8,6 +8,7 @@ const TOMORROW = 'tomorrow'; const LATER_THIS_WEEK = 'later_this_week'; const THIS_WEEKEND = 'this_weekend'; const NEXT_WEEK = 'next_week'; +const TWO_WEEKS = 'two_weeks'; const NEXT_MONTH = 'next_month'; const FOREVER = 'forever'; @@ -46,14 +47,13 @@ export default Combobox.extend({ }); } - if (day < 5) { + if (day < 5 && this.get('includeWeekend')) { selections.push({ id: THIS_WEEKEND, name: I18n.t('topic.auto_update_input.this_weekend') }); } - if (day !== 7) { selections.push({ id: NEXT_WEEK, @@ -61,6 +61,11 @@ export default Combobox.extend({ }); } + selections.push({ + id: TWO_WEEKS, + name: I18n.t('topic.auto_update_input.two_weeks') + }); + if (moment().endOf('month').date() !== now.date()) { selections.push({ id: NEXT_MONTH, @@ -127,7 +132,7 @@ export default Combobox.extend({ if (time) { if (state.id === LATER_TODAY) { time = time.format('h a'); - } else if (state.id === NEXT_MONTH) { + } else if (state.id === NEXT_MONTH || state.id === TWO_WEEKS) { time = time.format('MMM D'); } else { time = time.format('ddd, h a'); @@ -175,6 +180,10 @@ export default Combobox.extend({ time = time.add(1, 'week').day(1).hour(timeOfDay).minute(0); icon = 'briefcase'; break; + case TWO_WEEKS: + time = time.add(2, 'week').hour(timeOfDay).minute(0); + icon = 'briefcase'; + break; case NEXT_MONTH: time = time.add(1, 'month').startOf('month').hour(timeOfDay).minute(0); icon = 'briefcase'; diff --git a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs index ed205d9a00..62dd978281 100644 --- a/app/assets/javascripts/discourse/templates/components/future-date-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/future-date-input.hbs @@ -8,6 +8,7 @@ statusType=statusType value=selection input=input + includeWeekend=includeWeekend includeForever=includeForever width="50%" none="topic.auto_update_input.none"}} diff --git a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs index bc01b06634..dfe6826bdb 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs @@ -9,6 +9,7 @@ {{future-date-input input=updateTime statusType=selection + includeWeekend=true basedOnLastPost=false}} {{else if publishToCategory}}
@@ -21,12 +22,14 @@ {{future-date-input input=updateTime statusType=selection + includeWeekend=true categoryId=topicTimer.category_id basedOnLastPost=false}} {{else if autoClose}} {{future-date-input input=updateTime statusType=selection + includeWeekend=true basedOnLastPost=topicTimer.based_on_last_post lastPostedAt=model.last_posted_at}} {{/if}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e3a3f91f44..ecc0e9f5e4 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1568,6 +1568,7 @@ en: later_this_week: "Later this week" this_weekend: "This weekend" next_week: "Next week" + two_weeks: "Two Weeks" next_month: "Next month" forever: "Forever" pick_date_and_time: "Pick date and time" From db2bb96cf7cb51cd5e2f92bd3af10b456bf31367 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Mon, 2 Oct 2017 18:08:07 -0400 Subject: [PATCH 046/108] Update DEVELOPER-ADVANCED.md: use rake task to create first user * with the new startup wizard, you can no longer create a user with no admins present, so use the rake task instead --- docs/DEVELOPER-ADVANCED.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index a8bf7a1b84..4996bcd1ee 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -59,13 +59,9 @@ If everything goes alright, let's clone Discourse and start hacking: # launch discourse bundle exec rails s -b 0.0.0.0 # open browser on http://localhost:3000 and you should see Discourse -Create a test account, and enable it with: +Create an admin account with: - bundle exec rails c - u = User.find(1) - u.activate - u.grant_admin! - exit + bundle exec rake admin:create Discourse does a lot of stuff async, so it's better to run sidekiq even on development mode: From 90f36e7ab5aecebfcf7df57d83ac852975771ab7 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Mon, 2 Oct 2017 18:12:35 -0400 Subject: [PATCH 047/108] This was probably intended to be 'ruby $(which mailcatcher)' but it works without all that --- docs/DEVELOPER-ADVANCED.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index 4996bcd1ee..79c0d86a14 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -65,7 +65,7 @@ Create an admin account with: Discourse does a lot of stuff async, so it's better to run sidekiq even on development mode: - ruby $(mailcatcher) # open http://localhost:1080 to see the emails, stop with pkill -f mailcatcher + mailcatcher # open http://localhost:1080 to see the emails, stop with pkill -f mailcatcher bundle exec sidekiq # open http://localhost:3000/sidekiq to see queues bundle exec rails server From eaa896d8eeedbdd28637824cdb26ebf7ef983bc5 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 3 Oct 2017 11:20:08 +1100 Subject: [PATCH 048/108] FIX: not serving non brotli cdns from cdn_url (this means that access control allow origin could break) --- app/helpers/application_helper.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e7d1e7864e..7aa875341c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,6 +60,8 @@ module ApplicationHelper ENV["COMPRESS_BROTLI"] == "1" && request.env["HTTP_ACCEPT_ENCODING"] =~ /br/ path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") + elsif GlobalSetting.cdn_url + path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/cdn_asset/") end " ".html_safe From 81c009232689bb7c8cb64b135c7b15b94720a970 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 3 Oct 2017 10:52:28 +0800 Subject: [PATCH 049/108] Revert "FIX: not serving non brotli cdns from cdn_url" This reverts commit eaa896d8eeedbdd28637824cdb26ebf7ef983bc5. --- app/helpers/application_helper.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7aa875341c..e7d1e7864e 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -60,8 +60,6 @@ module ApplicationHelper ENV["COMPRESS_BROTLI"] == "1" && request.env["HTTP_ACCEPT_ENCODING"] =~ /br/ path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") - elsif GlobalSetting.cdn_url - path.gsub!("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/cdn_asset/") end " ".html_safe From 85c5bb4ea4e364dd5f2e8fbad32d8e3a6145de06 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 3 Oct 2017 11:59:26 +0800 Subject: [PATCH 050/108] Fix randomly failing spec. --- spec/controllers/uploads_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index f77645e543..f8cf43eb75 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -129,7 +129,7 @@ describe UploadsController do Jobs.expects(:enqueue).never - message = MessageBus.track_publish do + message = MessageBus.track_publish("/uploads/avatar") do post :create, params: { file: text_file, type: "avatar", format: :json } end.first From 3e53dbcade3c645e3cbd77610e4a5792a2c1a02e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 3 Oct 2017 13:54:50 +0800 Subject: [PATCH 051/108] UX: Only include tag hashtag postfix when necessary. https://meta.discourse.org/t/links-to-tags-not-working-in-final-post-unless-autocompleted/69884/6?u=tgxworld --- .../discourse/components/d-editor.js.es6 | 9 +-------- .../discourse/lib/category-tag-search.js.es6 | 20 +++++++++++++++++-- .../javascripts/discourse/lib/search.js.es6 | 8 +------- .../category-tag-autocomplete.raw.hbs | 2 +- lib/pretty_text/helpers.rb | 3 ++- spec/components/pretty_text_spec.rb | 18 +++++++++++------ 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 44f0130acb..d661b26a21 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -1,10 +1,7 @@ /*global Mousetrap:true */ import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; -import Category from 'discourse/models/category'; import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags'; -import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; import { search as searchCategoryTag } from 'discourse/lib/category-tag-search'; -import { SEPARATOR } from 'discourse/lib/category-hashtags'; import { cookAsync } from 'discourse/lib/text'; import { translations } from 'pretty-text/emoji/data'; import { emojiSearch, isSkinTonableEmoji } from 'pretty-text/emoji'; @@ -322,11 +319,7 @@ export default Ember.Component.extend({ template: findRawTemplate('category-tag-autocomplete'), key: '#', transformComplete(obj) { - if (obj.model) { - return Category.slugFor(obj.model, SEPARATOR); - } else { - return `${obj.text}${TAG_HASHTAG_POSTFIX}`; - } + return obj.text; }, dataSource(term) { return searchCategoryTag(term, siteSettings); diff --git a/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 index d7895ff47a..359e766041 100644 --- a/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/category-tag-search.js.es6 @@ -1,5 +1,7 @@ import { CANCELLED_STATUS } from 'discourse/lib/autocomplete'; import Category from 'discourse/models/category'; +import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; +import { SEPARATOR } from 'discourse/lib/category-hashtags'; var cache = {}; var cacheTime; @@ -27,7 +29,18 @@ function searchTags(term, categories, limit) { var returnVal = CANCELLED_STATUS; oldSearch.then((r) => { - var tags = r.results.map((tag) => { return { text: tag.text, count: tag.count }; }); + const categoryNames = cats.map(c => c.model.get('name')); + + const tags = r.results.map((tag) => { + const tagName = tag.text; + + return { + name: tagName, + text: (categoryNames.includes(tagName) ? `${tagName}${TAG_HASHTAG_POSTFIX}` : tagName), + count: tag.count, + }; + }); + returnVal = cats.concat(tags); }).always(() => { oldSearch = null; @@ -55,7 +68,10 @@ export function search(term, siteSettings) { const limit = 5; var categories = Category.search(term, { limit }); var numOfCategories = categories.length; - categories = categories.map((category) => { return { model: category }; }); + + categories = categories.map((category) => { + return { model: category, text: Category.slugFor(category, SEPARATOR) }; + }); if (numOfCategories !== limit && siteSettings.tagging_enabled) { return searchTags(term, categories, limit - numOfCategories); diff --git a/app/assets/javascripts/discourse/lib/search.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6 index 8939c17329..266f374a65 100644 --- a/app/assets/javascripts/discourse/lib/search.js.es6 +++ b/app/assets/javascripts/discourse/lib/search.js.es6 @@ -1,7 +1,5 @@ import { ajax } from 'discourse/lib/ajax'; import { findRawTemplate } from 'discourse/lib/raw-templates'; -import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; -import { SEPARATOR } from 'discourse/lib/category-hashtags'; import Category from 'discourse/models/category'; import { search as searchCategoryTag } from 'discourse/lib/category-tag-search'; import userSearch from 'discourse/lib/user-search'; @@ -148,11 +146,7 @@ export function applySearchAutocomplete($input, siteSettings, appEvents, options width: '100%', treatAsTextarea: true, transformComplete(obj) { - if (obj.model) { - return Category.slugFor(obj.model, SEPARATOR); - } else { - return `${obj.text}${TAG_HASHTAG_POSTFIX}`; - } + return obj.text; }, dataSource(term) { return searchCategoryTag(term, siteSettings); diff --git a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs index 1c429ddb39..3f4359aab5 100644 --- a/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/category-tag-autocomplete.raw.hbs @@ -5,7 +5,7 @@ {{#if option.model}} {{category-link option.model allowUncategorized="true" link="false"}} {{else}} - {{d-icon 'tag'}}{{option.text}} x {{option.count}} + {{d-icon 'tag'}}{{option.name}} x {{option.count}} {{/if}} {{/each}} diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index fcf66188f0..22383b3cec 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -91,7 +91,8 @@ module PrettyText if !is_tag && category = Category.query_from_hashtag_slug(text) [category.url_with_id, text] - elsif is_tag && tag = Tag.find_by_name(text.gsub!("#{tag_postfix}", '')) + elsif (!is_tag && tag = Tag.find_by(name: text)) || + (is_tag && tag = Tag.find_by(name: text.gsub!("#{tag_postfix}", ''))) ["#{Discourse.base_url}/tags/#{tag.name}", text] else nil diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 0cffc4862b..ec62b0aee2 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -717,16 +717,22 @@ describe PrettyText do expect(cooked).to eq(n expected) end - it "produces tag links" do + it "produces hashtag links" do + category = Fabricate(:category, name: 'testing') + category2 = Fabricate(:category, name: 'known') Fabricate(:topic, tags: [Fabricate(:tag, name: 'known')]) - cooked = PrettyText.cook(" #unknown::tag #known::tag") + cooked = PrettyText.cook(" #unknown::tag #known #known::tag #testing") - html = <<~HTML -

#unknown::tag #known

- HTML + [ + "#unknown::tag", + "#known", + "#known", + "#testing" + ].each do |element| - expect(cooked).to eq(html.strip) + expect(cooked).to include(element) + end cooked = PrettyText.cook("[`a` #known::tag here](http://somesite.com)") From 7e059a5a6eccd6b7b5094c2bf28e3ba0f910b1c3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 3 Oct 2017 14:56:44 +0800 Subject: [PATCH 052/108] Upgrade Rails to 5.1.4. --- Gemfile.lock | 65 ++++++++++++++++++++++++++-------------------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3b1aec0d30..a73a595cb5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,37 +1,37 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (5.1.3) - actionpack (= 5.1.3) - actionview (= 5.1.3) - activejob (= 5.1.3) + actionmailer (5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.1.3) - actionview (= 5.1.3) - activesupport (= 5.1.3) + actionpack (5.1.4) + actionview (= 5.1.4) + activesupport (= 5.1.4) rack (~> 2.0) - rack-test (~> 0.6.3) + rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.1.3) - activesupport (= 5.1.3) + actionview (5.1.4) + activesupport (= 5.1.4) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) active_model_serializers (0.8.3) activemodel (>= 3.0) - activejob (5.1.3) - activesupport (= 5.1.3) + activejob (5.1.4) + activesupport (= 5.1.4) globalid (>= 0.3.6) - activemodel (5.1.3) - activesupport (= 5.1.3) - activerecord (5.1.3) - activemodel (= 5.1.3) - activesupport (= 5.1.3) + activemodel (5.1.4) + activesupport (= 5.1.4) + activerecord (5.1.4) + activemodel (= 5.1.4) + activesupport (= 5.1.4) arel (~> 8.0) - activesupport (5.1.3) + activesupport (5.1.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) @@ -144,7 +144,8 @@ GEM rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) logster (1.2.7) - loofah (2.0.3) + loofah (2.1.1) + crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) mail (2.6.6) @@ -250,8 +251,8 @@ GEM ruby-openid (>= 2.1.8) rack-protection (2.0.0) rack - rack-test (0.6.3) - rack (>= 1.0) + rack-test (0.7.0) + rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) @@ -260,16 +261,16 @@ GEM rails_multisite (1.1.0.rc4) activerecord (> 4.2, < 6) railties (> 4.2, < 6) - railties (5.1.3) - actionpack (= 5.1.3) - activesupport (= 5.1.3) + railties (5.1.4) + actionpack (= 5.1.4) + activesupport (= 5.1.4) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake raindrops (0.18.0) - rake (12.0.0) + rake (12.1.0) rake-compiler (1.0.4) rake rb-fsevent (0.9.8) @@ -384,13 +385,13 @@ PLATFORMS ruby DEPENDENCIES - actionmailer (~> 5.1) - actionpack (~> 5.1) - actionview (~> 5.1) + actionmailer (~> 5.1.4) + actionpack (~> 5.1.4) + actionview (~> 5.1.4) active_model_serializers (~> 0.8.3) - activemodel (~> 5.1) - activerecord (~> 5.1) - activesupport (~> 5.1) + activemodel (~> 5.1.4) + activerecord (~> 5.1.4) + activesupport (~> 5.1.4) annotate aws-sdk barber @@ -457,7 +458,7 @@ DEPENDENCIES rack-mini-profiler rack-protection rails_multisite (~> 1.1.0.rc4) - railties (~> 5.1) + railties (~> 5.1.4) rake rb-fsevent rb-inotify (~> 0.9) From f1d8ed6aafbde872b45afb93c365a4e24ee0683c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 3 Oct 2017 14:59:25 +0800 Subject: [PATCH 053/108] Update lock file. --- Gemfile.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a73a595cb5..78ce5576a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -385,13 +385,13 @@ PLATFORMS ruby DEPENDENCIES - actionmailer (~> 5.1.4) - actionpack (~> 5.1.4) - actionview (~> 5.1.4) + actionmailer (~> 5.1) + actionpack (~> 5.1) + actionview (~> 5.1) active_model_serializers (~> 0.8.3) - activemodel (~> 5.1.4) - activerecord (~> 5.1.4) - activesupport (~> 5.1.4) + activemodel (~> 5.1) + activerecord (~> 5.1) + activesupport (~> 5.1) annotate aws-sdk barber @@ -458,7 +458,7 @@ DEPENDENCIES rack-mini-profiler rack-protection rails_multisite (~> 1.1.0.rc4) - railties (~> 5.1.4) + railties (~> 5.1) rake rb-fsevent rb-inotify (~> 0.9) From ac01885b60a53a4ca85c82163c9bb4b5df266264 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 3 Oct 2017 18:00:42 +1100 Subject: [PATCH 054/108] FEATURE: rake tasks for uploading assets to S3 This opens the door to serving application.js and so on from s3. Also updates s3 gem for some tagging support --- Gemfile | 2 +- Gemfile.lock | 19 ++++--- lib/s3_helper.rb | 60 ++++++++++++++++----- lib/tasks/s3.rake | 133 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+), 19 deletions(-) create mode 100644 lib/tasks/s3.rake diff --git a/Gemfile b/Gemfile index 7d39c4aff0..302d22be4c 100644 --- a/Gemfile +++ b/Gemfile @@ -55,7 +55,7 @@ gem 'fast_xor' # Forked until https://github.com/sdsykes/fastimage/pull/93 is merged gem 'discourse_fastimage', require: 'fastimage' -gem 'aws-sdk', require: false +gem 'aws-sdk-s3', require: false gem 'excon', require: false gem 'unf', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 78ce5576a9..8d33704952 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -44,12 +44,19 @@ GEM ansi (1.5.0) arel (8.0.0) ast (2.3.0) - aws-sdk (2.5.3) - aws-sdk-resources (= 2.5.3) - aws-sdk-core (2.5.3) + aws-partitions (1.24.0) + aws-sdk-core (3.6.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.5.3) - aws-sdk-core (= 2.5.3) + aws-sdk-kms (1.2.0) + aws-sdk-core (~> 3) + aws-sigv4 (~> 1.0) + aws-sdk-s3 (1.4.0) + aws-sdk-core (~> 3) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.0) + aws-sigv4 (1.0.2) barber (0.11.2) ember-source (>= 1.0, < 3) execjs (>= 1.2, < 3) @@ -393,7 +400,7 @@ DEPENDENCIES activerecord (~> 5.1) activesupport (~> 5.1) annotate - aws-sdk + aws-sdk-s3 barber better_errors binding_of_caller diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb index 778808a701..27348d631e 100644 --- a/lib/s3_helper.rb +++ b/lib/s3_helper.rb @@ -1,4 +1,4 @@ -require "aws-sdk" +require "aws-sdk-s3" class S3Helper @@ -46,21 +46,57 @@ class S3Helper rescue Aws::S3::Errors::NoSuchKey end - def update_tombstone_lifecycle(grace_period) - return if @tombstone_prefix.blank? + def update_lifecycle(id, days, prefix: nil) # cf. http://docs.aws.amazon.com/AmazonS3/latest/dev/object-lifecycle-mgmt.html + rule = { + id: id, + status: "Enabled", + expiration: { days: days } + } + + if prefix + rule[:prefix] = prefix + end + + rules = s3_resource.client.get_bucket_lifecycle_configuration(bucket: @s3_bucket_name).rules + + rules.delete_if do |r| + r.id == id + end + + rules.map! { |r| r.to_h } + + rules << rule + s3_resource.client.put_bucket_lifecycle(bucket: @s3_bucket_name, lifecycle_configuration: { - rules: [ - { - id: "purge-tombstone", - status: "Enabled", - expiration: { days: grace_period }, - prefix: @tombstone_prefix - } - ] - }) + rules: rules + }) + end + + def update_tombstone_lifecycle(grace_period) + return if @tombstone_prefix.blank? + update_lifecycle("purge_tombstone", grace_period, prefix: @tombstone_prefix) + end + + def list + s3_bucket.objects(prefix: @s3_bucket_folder_path) + end + + def tag_file(key, tags) + tag_array = [] + tags.each do |k, v| + tag_array << { key: k.to_s, value: v.to_s } + end + + s3_resource.client.put_object_tagging( + bucket: @s3_bucket_name, + key: key, + tagging: { + tag_set: tag_array + } + ) end private diff --git a/lib/tasks/s3.rake b/lib/tasks/s3.rake new file mode 100644 index 0000000000..8242493a68 --- /dev/null +++ b/lib/tasks/s3.rake @@ -0,0 +1,133 @@ +require_dependency "s3_helper" + +def brotli_s3_path(path) + ext = File.extname(path) + "#{path[0..-ext.length]}br#{ext}" +end + +def gzip_s3_path(path) + ext = File.extname(path) + "#{path[0..-ext.length]}gz#{ext}" +end + +def should_skip?(path) + return true if ENV['FORCE_S3_UPLOADS'] + @existing_assets ||= Set.new(helper.list.map(&:key)) + @existing_assets.include?('assets/' + path) +end + +def upload_asset(helper, path, recurse: true, content_type: nil, fullpath: nil, content_encoding: nil) + fullpath ||= (Rails.root + "public/assets/#{path}").to_s + + content_type ||= MiniMime.lookup_by_filename(path).content_type + + options = { + cache_control: 'max-age=31556952, public, immutable', + content_type: content_type, + acl: 'public-read', + tagging: '' + } + + if content_encoding + options[:content_encoding] = content_encoding + end + + if should_skip?(path) + puts "Skipping: #{path}" + else + puts "Uploading: #{path}" + helper.upload(fullpath, path, options) + end + + if recurse + if File.exist?(fullpath + ".br") + brotli_path = brotli_s3_path(path) + upload_asset(helper, brotli_path, + fullpath: fullpath + ".br", + recurse: false, + content_type: content_type, + content_encoding: 'br' + ) + end + + if File.exist?(fullpath + ".gz") + gzip_path = gzip_s3_path(path) + upload_asset(helper, gzip_path, + fullpath: fullpath + ".gz", + recurse: false, + content_type: content_type, + content_encoding: 'gzip' + ) + end + + if File.exist?(fullpath + ".map") + upload_asset(helper, path + ".map", recurse: false, content_type: 'application/json') + end + end +end + +def assets + cached = Rails.application.assets.cached + manifest = Sprockets::Manifest.new(cached, Rails.root + 'public/assets', Rails.application.config.assets.manifest) + + raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.s3_upload_bucket.blank? + manifest.assets +end + +def helper + @helper ||= S3Helper.new(SiteSetting.s3_upload_bucket.downcase + '/assets') +end + +def in_manifest + found = [] + assets.each do |_, path| + fullpath = (Rails.root + "public/assets/#{path}").to_s + + asset_path = "assets/#{path}" + found << asset_path + + if File.exist?(fullpath + '.br') + found << brotli_s3_path(asset_path) + end + + if File.exist?(fullpath + '.gz') + found << gzip_s3_path(asset_path) + end + + if File.exist?(fullpath + '.map') + found << asset_path + '.map' + end + + end + Set.new(found) +end + +task 's3:upload_assets' => :environment do + assets.each do |name, fingerprint| + upload_asset(helper, fingerprint) + end +end + +task 's3:expire_missing_assets' => :environment do + keep = in_manifest + + count = 0 + puts "Ensuring AWS assets are tagged correctly for removal" + helper.list.each do |f| + if keep.include?(f.key) + helper.tag_file(f.key, old: true) + count += 1 + else + # ensure we do not delete this by mistake + helper.tag_file(f.key, {}) + end + end + + puts "#{count} assets were flagged for removal in 10 days" + + puts "Ensuring AWS rule exists for purging old assets" + #helper.update_lifecycle("delete_old_assets", 10, prefix: 'old=true') + + puts "Waiting on https://github.com/aws/aws-sdk-ruby/issues/1623" + +end From 5b96463c4055e2c8f33eb91d720aec976101e838 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 3 Oct 2017 18:27:09 +1100 Subject: [PATCH 055/108] in production there is no cached it seems --- lib/tasks/s3.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/s3.rake b/lib/tasks/s3.rake index 8242493a68..a26c89ec74 100644 --- a/lib/tasks/s3.rake +++ b/lib/tasks/s3.rake @@ -67,7 +67,7 @@ def upload_asset(helper, path, recurse: true, content_type: nil, fullpath: nil, end def assets - cached = Rails.application.assets.cached + cached = Rails.application.assets&.cached manifest = Sprockets::Manifest.new(cached, Rails.root + 'public/assets', Rails.application.config.assets.manifest) raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.s3_upload_bucket.blank? From daf1dda700823d270177f96cc9b1714806bd85a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 3 Oct 2017 12:49:45 +0200 Subject: [PATCH 056/108] FIX: username autocomplete in assign modal wasn't working --- app/controllers/users_controller.rb | 6 ++++-- spec/requests/users_controller_spec.rb | 22 ++++++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index b71d9f6305..9a064933d4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -706,8 +706,10 @@ class UsersController < ApplicationController Group.messageable(current_user) end - to_render[:groups] = groups.where("name ILIKE :term_like", term_like: "#{term}%") - .map { |m| { name: m.name, full_name: m.full_name } } + if groups + to_render[:groups] = groups.where("name ILIKE :term_like", term_like: "#{term}%") + .map { |m| { name: m.name, full_name: m.full_name } } + end end render json: to_render diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 13bfab2d44..0f3586b2cb 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -175,6 +175,16 @@ RSpec.describe UsersController do sign_in(user) end + it "doesn't search for groups" do + get "/u/search/users.json", params: { + include_mentionable_groups: 'false', + include_messageable_groups: 'false' + } + + expect(response).to be_success + expect(JSON.parse(response.body)).not_to have_key(:groups) + end + it "searches for messageable groups" do get "/u/search/users.json", params: { include_mentionable_groups: 'false', @@ -198,13 +208,21 @@ RSpec.describe UsersController do describe 'when not signed in' do it 'should not include mentionable/messageable groups' do + get "/u/search/users.json", params: { + include_mentionable_groups: 'false', + include_messageable_groups: 'false' + } + + expect(response).to be_success + expect(JSON.parse(response.body)).not_to have_key(:groups) + get "/u/search/users.json", params: { include_mentionable_groups: 'false', include_messageable_groups: 'true' } expect(response).to be_success - expect(JSON.parse(response.body)["groups"]).to eq(nil) + expect(JSON.parse(response.body)).not_to have_key(:groups) get "/u/search/users.json", params: { include_messageable_groups: 'false', @@ -212,7 +230,7 @@ RSpec.describe UsersController do } expect(response).to be_success - expect(JSON.parse(response.body)["groups"]).to eq(nil) + expect(JSON.parse(response.body)).not_to have_key(:groups) end end end From fafe7cc661bbe5663522a6a9e650f67892abeedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 3 Oct 2017 13:02:04 +0200 Subject: [PATCH 057/108] remove trailing whitespaces --- spec/requests/users_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 0f3586b2cb..002d3cc5b2 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -215,7 +215,7 @@ RSpec.describe UsersController do expect(response).to be_success expect(JSON.parse(response.body)).not_to have_key(:groups) - + get "/u/search/users.json", params: { include_mentionable_groups: 'false', include_messageable_groups: 'true' From 93bd03f7e020de5837f5bbf1d54b465e3b252c55 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Tue, 3 Oct 2017 10:25:08 -0400 Subject: [PATCH 058/108] DEVELOPER-ADVANCED.md: update instructions to use rake tasks instead of (outdated) manual commands --- docs/DEVELOPER-ADVANCED.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md index 79c0d86a14..9b417e5d99 100644 --- a/docs/DEVELOPER-ADVANCED.md +++ b/docs/DEVELOPER-ADVANCED.md @@ -26,13 +26,9 @@ To get your Ubuntu 16.04 LTS install up and running to develop Discourse and Dis gem install bundler mailcatcher # Postgresql - sudo su postgres - createuser --createdb --superuser -Upostgres $(cat /tmp/username) + sudo -u postgres -i + createuser --superuser -Upostgres $(cat /tmp/username) psql -c "ALTER USER $(cat /tmp/username) WITH PASSWORD 'password';" - psql -c "create database discourse_development owner $(cat /tmp/username) encoding 'UTF8' TEMPLATE template0;" - psql -c "create database discourse_test owner $(cat /tmp/username) encoding 'UTF8' TEMPLATE template0;" - psql -d discourse_development -c "CREATE EXTENSION hstore;" - psql -d discourse_development -c "CREATE EXTENSION pg_trgm;" exit # Node @@ -50,8 +46,14 @@ If everything goes alright, let's clone Discourse and start hacking: git clone https://github.com/discourse/discourse.git ~/discourse cd ~/discourse bundle install - bundle exec rake db:migrate - RAILS_ENV=test bundle exec rake db:migrate + + # run this if there was a pre-existing database + bundle exec rake db:drop + RAILS_ENV=test bundle exec rake db:drop + + # time to create the database and run migrations + bundle exec rake db:create db:migrate + RAILS_ENV=test bundle exec rake db:create db:migrate # run the specs (optional) bundle exec rake autospec # CTRL + C to stop @@ -63,6 +65,12 @@ Create an admin account with: bundle exec rake admin:create +If you ever need to recreate your database: + + bundle exec rake db:drop db:create db:migrate + bundle exec rake admin:create + RAILS_ENV=test bundle exec rake db:drop db:create db:migrate + Discourse does a lot of stuff async, so it's better to run sidekiq even on development mode: mailcatcher # open http://localhost:1080 to see the emails, stop with pkill -f mailcatcher From 76706f91447de810f6aa3b618a2f5fe8c0924b4b Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 3 Oct 2017 10:13:19 +0200 Subject: [PATCH 059/108] FIX: don't create staged users when incoming email is rejected FIX: don't send subscription mail to new users --- config/locales/server.en.yml | 1 + lib/email/processor.rb | 3 +- lib/email/receiver.rb | 49 ++++++++++++++----- spec/components/email/receiver_spec.rb | 44 +++++++++++++++++ spec/fixtures/emails/unsubscribe_new_user.eml | 11 +++++ 5 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 spec/fixtures/emails/unsubscribe_new_user.eml diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index dbcb7f8ba1..245b329690 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -79,6 +79,7 @@ en: topic_closed_error: "Happens when a reply came in but the related topic has been closed." bounced_email_error: "Email is a bounced email report." screened_email_error: "Happens when the sender's email address was already screened." + unsubscribe_not_allowed: "Happens when unsubscribing via email is not allowed for this user." unrecognized_error: "Unrecognized Error" errors: &errors diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 20d2704da6..f1ee881531 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -49,7 +49,8 @@ module Email when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found when Email::Receiver::TopicClosedError then :email_reject_topic_closed - when Email::Receiver::InvalidPost then :email_reject_invalid_post + when Email::Receiver::InvalidPost then :email_reject_invalid_pos + when Email::Receiver::UnsubscribeNotAllowed then :email_reject_invalid_post when ActiveRecord::Rollback then :email_reject_invalid_post when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action when Discourse::InvalidAccess then :email_reject_invalid_access diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 7fae5ec3b5..8405bde4a4 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -30,6 +30,7 @@ module Email class TopicClosedError < ProcessingError; end class InvalidPost < ProcessingError; end class InvalidPostAction < ProcessingError; end + class UnsubscribeNotAllowed < ProcessingError; end attr_reader :incoming_email attr_reader :raw_email @@ -38,7 +39,7 @@ module Email def initialize(mail_string) raise EmptyEmailError if mail_string.blank? - @staged_users_created = 0 + @staged_users = [] @raw_email = try_to_encode(mail_string, "UTF-8") || try_to_encode(mail_string, "ISO-8859-1") || mail_string @mail = Mail.new(@raw_email) @message_id = @mail.message_id.presence || Digest::MD5.hexdigest(mail_string) @@ -82,14 +83,13 @@ module Email raise NoSenderDetectedError if @from_email.blank? raise ScreenedEmailError if ScreenedEmail.should_block?(@from_email) - user = find_or_create_user(@from_email, @from_display_name) + user = find_user(@from_email) - raise UserNotFoundError if user.nil? - - @incoming_email.update_columns(user_id: user.id) - - raise InactiveUserError if !user.active && !user.staged - raise BlockedUserError if user.blocked + if user.present? + process_user(user) + else + raise UserNotFoundError unless SiteSetting.enable_staged_users + end body, elided = select_body body ||= "" @@ -102,9 +102,17 @@ module Email end if action = subscription_action_for(body, subject) - message = SubscriptionMailer.send(action, user) - Email::Sender.new(message, :subscription).send - elsif post = find_related_post + raise UnsubscribeNotAllowed if user.nil? + send_subscription_mail(action, user) + return + end + + # Lets create a staged user if there isn't one yet. We will try to + # delete staged users in process!() if something bad happens. + user = find_or_create_user(@from_email, @from_display_name) if user.nil? + process_user(user) + + if post = find_related_post create_reply(user: user, raw: body, elided: elided, @@ -128,6 +136,13 @@ module Email end end + def process_user(user) + @incoming_email.update_columns(user_id: user.id) + + raise InactiveUserError if !user.active && !user.staged + raise BlockedUserError if user.blocked + end + def is_bounce? return false unless @mail.bounced? || verp @@ -310,6 +325,10 @@ module Email @suject ||= @mail.subject.presence || I18n.t("emails.incoming.default_subject", email: @from_email) end + def find_user(email) + User.find_by_email(email) + end + def find_or_create_user(email, display_name) user = nil @@ -325,7 +344,7 @@ module Email name: display_name.presence || User.suggest_name(email), staged: true ) - @staged_users_created += 1 + @staged_users << user end rescue user = nil @@ -693,7 +712,7 @@ module Email topic.add_small_action(sender, "invited_user", user.username) end # cap number of staged users created per email - if @staged_users_created > SiteSetting.maximum_staged_users_per_email + if @staged_users.count > SiteSetting.maximum_staged_users_per_email topic.add_moderator_post(sender, I18n.t("emails.incoming.maximum_staged_user_per_email_reached")) return end @@ -717,6 +736,10 @@ module Email !topic.topic_allowed_groups.where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id).exists? end + def send_subscription_mail(action, user) + message = SubscriptionMailer.send(action, user) + Email::Sender.new(message, :subscription).send + end end end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 4a13ac13b0..40dc01a0f3 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -300,6 +300,12 @@ describe Email::Receiver do expect(before_deliveries).to eq ActionMailer::Base.deliveries.count end end + + it "raises an UnsubscribeNotAllowed and does not send an unsubscribe email" do + before_deliveries = ActionMailer::Base.deliveries.count + expect { process(:unsubscribe_new_user) }.to raise_error { Email::Receiver::UnsubscribeNotAllowed } + expect(before_deliveries).to eq ActionMailer::Base.deliveries.count + end end it "handles inline reply" do @@ -623,4 +629,42 @@ describe Email::Receiver do end end + context "no staged users on error" do + before do + SiteSetting.enable_staged_users = true + end + + shared_examples "no staged users" do |email_name| + it "does not create staged users" do + staged_user_count = User.where(staged: true).count + process(email_name) rescue nil + expect(User.where(staged: true).count).to eq(staged_user_count) + end + end + + context "when email address is screened" do + before do + ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true) + end + + include_examples "no staged users", :screened_email + end + + context "when the mail is auto generated" do + include_examples "no staged users", :auto_generated_header + end + + context "when email is a bounced email" do + include_examples "no staged users", :bounced_email + end + + context "when the body is blank" do + include_examples "no staged users", :no_body + end + + context "when unsubscribe via email is not allowed" do + include_examples "no staged users", :unsubscribe_new_user + end + end + end diff --git a/spec/fixtures/emails/unsubscribe_new_user.eml b/spec/fixtures/emails/unsubscribe_new_user.eml new file mode 100644 index 0000000000..337f2c88b6 --- /dev/null +++ b/spec/fixtures/emails/unsubscribe_new_user.eml @@ -0,0 +1,11 @@ +Return-Path: +From: Foo Bar +To: reply@bar.com +Date: Thu, 13 Jun 2013 17:03:48 -0400 +Message-ID: <56@foo.bar.mail> +Subject: UnSuBScRiBe +Mime-Version: 1.0 +Content-Type: text/plain; +Content-Transfer-Encoding: 7bit + +I've basically had enough of your mailing list and would very much like it if you went away. From 7f50380221f4a77bec7dfedc141ef99213de9438 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 3 Oct 2017 11:23:18 +0200 Subject: [PATCH 060/108] FIX: respect email domain whitelist/blacklist when creating staged users --- config/locales/server.en.yml | 9 ++++++ lib/email/processor.rb | 1 + lib/email/receiver.rb | 29 +++++++++++-------- lib/validators/email_validator.rb | 25 +++++++++------- spec/components/email/receiver_spec.rb | 27 +++++++++++++++++ .../validators/email_validator_spec.rb | 8 +++++ .../emails/blacklist_whitelist_email.eml | 9 ++++++ 7 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 spec/fixtures/emails/blacklist_whitelist_email.eml diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 245b329690..374123e632 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -80,6 +80,7 @@ en: bounced_email_error: "Email is a bounced email report." screened_email_error: "Happens when the sender's email address was already screened." unsubscribe_not_allowed: "Happens when unsubscribing via email is not allowed for this user." + email_not_allowed: "Happens when the email address is not on the whitelist or is on the blacklist." unrecognized_error: "Unrecognized Error" errors: &errors @@ -2151,6 +2152,14 @@ en: Your reply was sent from a blocked email address. Try sending from another email address, or [contact a staff member](%{base_url}/about). + email_reject_not_allowed_email: + title: "Email Reject Not Allowed Email" + subject_template: "[%{email_prefix}] Email issue -- Blocked Email" + text_body_template: | + We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. + + Your reply was sent from a blocked email address. Try sending from another email address, or [contact a staff member](%{base_url}/about). + email_reject_inactive_user: title: "Email Reject Inactive User" subject_template: "[%{email_prefix}] Email issue -- Inactive User" diff --git a/lib/email/processor.rb b/lib/email/processor.rb index f1ee881531..b57fb9a003 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -40,6 +40,7 @@ module Email when Email::Receiver::NoBodyDetectedError then :email_reject_empty when Email::Receiver::UserNotFoundError then :email_reject_user_not_found when Email::Receiver::ScreenedEmailError then :email_reject_screened_email + when Email::Receiver::EmailNotAllowed then :email_reject_not_allowed_email when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated when Email::Receiver::InactiveUserError then :email_reject_inactive_user when Email::Receiver::BlockedUserError then :email_reject_blocked_user diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 8405bde4a4..69569f82b5 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -31,6 +31,7 @@ module Email class InvalidPost < ProcessingError; end class InvalidPostAction < ProcessingError; end class UnsubscribeNotAllowed < ProcessingError; end + class EmailNotAllowed < ProcessingError; end attr_reader :incoming_email attr_reader :raw_email @@ -86,7 +87,7 @@ module Email user = find_user(@from_email) if user.present? - process_user(user) + log_and_validate_user(user) else raise UserNotFoundError unless SiteSetting.enable_staged_users end @@ -109,8 +110,10 @@ module Email # Lets create a staged user if there isn't one yet. We will try to # delete staged users in process!() if something bad happens. - user = find_or_create_user(@from_email, @from_display_name) if user.nil? - process_user(user) + if user.nil? + user = find_or_create_user(@from_email, @from_display_name) + log_and_validate_user(user) + end if post = find_related_post create_reply(user: user, @@ -136,11 +139,11 @@ module Email end end - def process_user(user) + def log_and_validate_user(user) @incoming_email.update_columns(user_id: user.id) raise InactiveUserError if !user.active && !user.staged - raise BlockedUserError if user.blocked + raise BlockedUserError if user.blocked end def is_bounce? @@ -333,10 +336,12 @@ module Email user = nil User.transaction do - begin - user = User.find_by_email(email) + user = User.find_by_email(email) - if user.nil? && SiteSetting.enable_staged_users + if user.nil? && SiteSetting.enable_staged_users + raise EmailNotAllowed unless EmailValidator.allowed?(email) + + begin username = UserNameSuggester.sanitize_username(display_name) if display_name.present? user = User.create!( email: email, @@ -345,9 +350,9 @@ module Email staged: true ) @staged_users << user + rescue + user = nil end - rescue - user = nil end end @@ -717,8 +722,8 @@ module Email return end end - rescue ActiveRecord::RecordInvalid - # don't care if user already allowed + rescue ActiveRecord::RecordInvalid, EmailNotAllowed + # don't care if user already allowed or the user's email address is not allowed end end end diff --git a/lib/validators/email_validator.rb b/lib/validators/email_validator.rb index 468c005071..64463d53cb 100644 --- a/lib/validators/email_validator.rb +++ b/lib/validators/email_validator.rb @@ -1,27 +1,32 @@ class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - if (setting = SiteSetting.email_domains_whitelist).present? - unless email_in_restriction_setting?(setting, value) || is_developer?(value) - record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) - end - elsif (setting = SiteSetting.email_domains_blacklist).present? - if email_in_restriction_setting?(setting, value) && !is_developer?(value) - record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) - end + unless EmailValidator.allowed?(value) + record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) end + if record.errors[attribute].blank? && value && ScreenedEmail.should_block?(value) record.errors.add(attribute, I18n.t(:'user.email.blocked')) end end - def email_in_restriction_setting?(setting, value) + def self.allowed?(email) + if (setting = SiteSetting.email_domains_whitelist).present? + return email_in_restriction_setting?(setting, email) || is_developer?(email) + elsif (setting = SiteSetting.email_domains_blacklist).present? + return !(email_in_restriction_setting?(setting, email) && !is_developer?(email)) + end + + true + end + + def self.email_in_restriction_setting?(setting, value) domains = setting.gsub('.', '\.') regexp = Regexp.new("@(.+\\.)?(#{domains})", true) value =~ regexp end - def is_developer?(value) + def self.is_developer?(value) Rails.configuration.respond_to?(:developer_emails) && Rails.configuration.developer_emails.include?(value) end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 40dc01a0f3..839f36192f 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -23,6 +23,16 @@ describe Email::Receiver do expect { process(:screened_email) }.to raise_error(Email::Receiver::ScreenedEmailError) end + it "raises EmailNotAllowed when email address is not on whitelist" do + SiteSetting.email_domains_whitelist = "example.com|bar.com" + expect { process(:blacklist_whitelist_email) }.to raise_error(Email::Receiver::EmailNotAllowed) + end + + it "raises EmailNotAllowed when email address is on blacklist" do + SiteSetting.email_domains_blacklist = "email.com|mail.com" + expect { process(:blacklist_whitelist_email) }.to raise_error(Email::Receiver::EmailNotAllowed) + end + it "raises an UserNotFoundError when staged users are disabled" do SiteSetting.enable_staged_users = false expect { process(:user_not_found) }.to raise_error(Email::Receiver::UserNotFoundError) @@ -665,6 +675,23 @@ describe Email::Receiver do context "when unsubscribe via email is not allowed" do include_examples "no staged users", :unsubscribe_new_user end + + context "when email address is not on whitelist" do + before do + SiteSetting.email_domains_whitelist = "example.com|bar.com" + end + + include_examples "no staged users", :blacklist_whitelist_email + end + + context "when email address is on blacklist" do + before do + SiteSetting.email_domains_blacklist = "email.com|mail.com" + end + + include_examples "no staged users", :blacklist_whitelist_email + end + end end diff --git a/spec/components/validators/email_validator_spec.rb b/spec/components/validators/email_validator_spec.rb index 5b06cafe57..71f6bb746a 100644 --- a/spec/components/validators/email_validator_spec.rb +++ b/spec/components/validators/email_validator_spec.rb @@ -30,6 +30,14 @@ describe EmailValidator do expect(blocks?('sam@e-mail.com')).to eq(true) expect(blocks?('sam@googlemail.com')).to eq(false) end + + it "blocks based on email_domains_whitelist" do + SiteSetting.email_domains_whitelist = "googlemail.com|email.com" + expect(blocks?('sam@email.com')).to eq(false) + expect(blocks?('sam@bob.email.com')).to eq(false) + expect(blocks?('sam@e-mail.com')).to eq(true) + expect(blocks?('sam@googlemail.com')).to eq(false) + end end context '.email_regex' do diff --git a/spec/fixtures/emails/blacklist_whitelist_email.eml b/spec/fixtures/emails/blacklist_whitelist_email.eml new file mode 100644 index 0000000000..f6b38bfcce --- /dev/null +++ b/spec/fixtures/emails/blacklist_whitelist_email.eml @@ -0,0 +1,9 @@ +Return-Path: +From: Foo +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <51@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Email from a domain on blacklist or whitelist. From 4fbe4218c42b91774518cfbe20b169b2f503348a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 3 Oct 2017 13:01:05 -0400 Subject: [PATCH 061/108] FIX: Header primary color was too dark in dark mode --- app/models/color_scheme.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index c0a01cc5d5..0e1f09c12c 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -9,7 +9,7 @@ class ColorScheme < ActiveRecord::Base "tertiary" => '0f82af', "quaternary" => 'c14924', "header_background" => '111111', - "header_primary" => '333333', + "header_primary" => 'dddddd', "highlight" => 'a87137', "danger" => 'e45735', "success" => '1ca551', From 4b7256d2e4126fbf48055d1d2d5b8119e7b70de2 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 3 Oct 2017 14:31:25 -0400 Subject: [PATCH 062/108] FIX: `d-header` in common is `z-index: 1001` --- app/assets/stylesheets/desktop/header.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/header.scss b/app/assets/stylesheets/desktop/header.scss index 4c4d3da302..c401a76594 100644 --- a/app/assets/stylesheets/desktop/header.scss +++ b/app/assets/stylesheets/desktop/header.scss @@ -4,7 +4,6 @@ .d-header { left: 0; - z-index: 1000; padding-top: 3px; height: 60px; .d-icon-home { From c72ceb1f2dcbdc338114cb4f6f99b1224a08b7f5 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 3 Oct 2017 14:39:56 -0400 Subject: [PATCH 063/108] An option to not display categories in the hamburger This is mostly useful if your site has very few categories. --- .../discourse/widgets/hamburger-menu.js.es6 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 890a7a7b4a..9dff50d323 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -33,6 +33,10 @@ createWidget('priority-faq-link', { export default createWidget('hamburger-menu', { tagName: 'div.hamburger-panel', + settings: { + showCategories: true + }, + adminLinks() { const { currentUser } = this; @@ -176,8 +180,12 @@ export default createWidget('hamburger-menu', { } results.push(this.attach('menu-links', {name: 'general-links', contents: () => this.generalLinks() })); - results.push(this.listCategories()); - results.push(h('hr')); + + if (this.settings.showCategories) { + results.push(this.listCategories()); + results.push(h('hr')); + } + results.push(this.attach('menu-links', {name: 'footer-links', omitRule: true, contents: () => this.footerLinks(prioritizeFaq, faqUrl) })); return results; From cc4a102b2674615dde3657484ca67e61da8fb139 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 3 Oct 2017 15:24:18 -0400 Subject: [PATCH 064/108] UX: Allow customization on header dropdown sizes --- .../javascripts/discourse/widgets/hamburger-menu.js.es6 | 8 ++++++-- .../javascripts/discourse/widgets/user-menu.js.es6 | 9 ++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 9dff50d323..b5835b3c19 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -34,7 +34,8 @@ export default createWidget('hamburger-menu', { tagName: 'div.hamburger-panel', settings: { - showCategories: true + showCategories: true, + maxWidth: 300 }, adminLinks() { @@ -192,7 +193,10 @@ export default createWidget('hamburger-menu', { }, html() { - return this.attach('menu-panel', { contents: () => this.panelContents() }); + return this.attach('menu-panel', { + contents: () => this.panelContents(), + maxWidth: this.settings.maxWidth, + }); }, clickOutside() { diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index db8207c688..6ba446b8be 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -89,6 +89,10 @@ createWidget('user-menu-links', { export default createWidget('user-menu', { tagName: 'div.user-menu', + settings: { + maxWidth: 300 + }, + panelContents() { const path = this.currentUser.get('path'); @@ -104,7 +108,10 @@ export default createWidget('user-menu', { }, html() { - return this.attach('menu-panel', { contents: () => this.panelContents() }); + return this.attach('menu-panel', { + maxWidth: this.settings.maxWidth, + contents: () => this.panelContents() + }); }, clickOutside() { From e47f5cedd23d5979e3e8dadb51cf308bc747a536 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 3 Oct 2017 14:08:37 -0400 Subject: [PATCH 065/108] FEATURE: forgot_password_strict setting also prevents reporting that an email address is taken during signup --- app/controllers/users_controller.rb | 13 +++++++++++++ app/mailers/user_notifications.rb | 9 +++++++++ app/services/user_activator.rb | 17 +++++++++++++++++ config/locales/server.en.yml | 13 +++++++++++++ spec/controllers/users_controller_spec.rb | 22 ++++++++++++++++++++++ 5 files changed, 74 insertions(+) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 9a064933d4..69ab62df52 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -372,6 +372,19 @@ class UsersController < ApplicationController message: activation.message, user_id: user.id } + elsif SiteSetting.forgot_password_strict && user.errors[:primary_email]&.include?(I18n.t('errors.messages.taken')) + session["user_created_message"] = activation.success_message + + if existing_user = User.find_by_email(user.primary_email&.email) + Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id) + end + + render json: { + success: true, + active: user.active?, + message: activation.success_message, + user_id: user.id + } else errors = user.errors.to_hash errors[:email] = errors.delete(:primary_email) if errors[:primary_email] diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 83ba60ad08..e1d24b37c6 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -83,6 +83,15 @@ class UserNotifications < ActionMailer::Base ) end + def account_exists(user, opts = {}) + build_email( + user.email, + template: 'user_notifications.account_exists', + locale: user_locale(user), + email: user.email + ) + end + def short_date(dt) if dt.year == Time.now.year I18n.l(dt, format: :short_no_year) diff --git a/app/services/user_activator.rb b/app/services/user_activator.rb index d0c380db95..d65fba83c2 100644 --- a/app/services/user_activator.rb +++ b/app/services/user_activator.rb @@ -16,6 +16,10 @@ class UserActivator @message = activator.activate end + def success_message + activator.success_message + end + private def activator @@ -38,6 +42,10 @@ end class ApprovalActivator < UserActivator def activate + success_message + end + + def success_message I18n.t("login.wait_approval") end end @@ -52,6 +60,11 @@ class EmailActivator < UserActivator user_id: user.id, email_token: email_token.token ) + + success_message + end + + def success_message I18n.t("login.activate_email", email: Rack::Utils.escape_html(user.email)) end end @@ -62,6 +75,10 @@ class LoginActivator < UserActivator def activate log_on_user(user) user.enqueue_welcome_message('welcome_user') + success_message + end + + def success_message I18n.t("login.active") end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 374123e632..d28b259caa 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2656,6 +2656,19 @@ en: %{message} + account_exists: + title: "Account already exists" + subject_template: "[%{email_prefix}] Account already exists" + text_body_template: | + You just tried to create an account at %{site_name}. However, an account already exists for %{email}. + + If you forgot your password, [reset it now](%{base_url}/password-reset). + + If you didn’t try to create an account for %{email}, don’t worry – you can safely ignore this message. + + If you have any questions, [contact our friendly staff](%{base_url}/about). + + digest: why: "A brief summary of %{site_link} since your last visit on %{last_seen_at}" diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index a16779d4c2..fcd265fef3 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -621,6 +621,28 @@ describe UsersController do expect(session[SessionController::ACTIVATE_USER_KEY]).to be_present end end + + context 'users already exists with given email' do + let!(:existing) { Fabricate(:user, email: post_user_params[:email]) } + + it 'returns an error if forgot_password_strict is disabled' do + SiteSetting.forgot_password_strict = false + post_user + json = JSON.parse(response.body) + expect(json['success']).to eq(false) + expect(json['message']).to be_present + end + + it 'returns success if forgot_password_strict is enabled' do + SiteSetting.forgot_password_strict = true + expect { + post_user + }.to_not change { User.count } + json = JSON.parse(response.body) + expect(json['active']).to be_falsey + expect(session["user_created_message"]).to be_present + end + end end context "creating as active" do From 1faae3c7657251e4d09bc3fb19f90bfe1a5fe35f Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 3 Oct 2017 15:28:15 -0400 Subject: [PATCH 066/108] rename forgot_password_strict to hide_email_address_taken --- app/controllers/session_controller.rb | 2 +- app/controllers/users_controller.rb | 2 +- config/locales/server.ar.yml | 2 +- config/locales/server.de.yml | 2 +- config/locales/server.el.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.it.yml | 2 +- config/locales/server.ko.yml | 2 +- config/locales/server.pl_PL.yml | 2 +- config/locales/server.pt.yml | 2 +- config/locales/server.pt_BR.yml | 2 +- config/locales/server.ro.yml | 2 +- config/locales/server.sv.yml | 2 +- config/locales/server.tr_TR.yml | 2 +- config/locales/server.zh_CN.yml | 2 +- config/locales/server.zh_TW.yml | 2 +- config/site_settings.yml | 2 +- ...171003180951_rename_forgot_password_strict_setting.rb | 9 +++++++++ spec/controllers/users_controller_spec.rb | 8 ++++---- 24 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 db/migrate/20171003180951_rename_forgot_password_strict_setting.rb diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 1060c6e034..c67e6baf34 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -247,7 +247,7 @@ class SessionController < ApplicationController end json = { result: "ok" } - unless SiteSetting.forgot_password_strict + unless SiteSetting.hide_email_address_taken json[:user_found] = user_presence end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 69ab62df52..ca3dba2426 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -372,7 +372,7 @@ class UsersController < ApplicationController message: activation.message, user_id: user.id } - elsif SiteSetting.forgot_password_strict && user.errors[:primary_email]&.include?(I18n.t('errors.messages.taken')) + elsif SiteSetting.hide_email_address_taken && user.errors[:primary_email]&.include?(I18n.t('errors.messages.taken')) session["user_created_message"] = activation.success_message if existing_user = User.find_by_email(user.primary_email&.email) diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index eaaf22fead..0dcc76db84 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -1037,7 +1037,7 @@ ar: allow_index_in_robots_txt: "حدّد في robots.txt إمكانيّة فهرسة محرّكات البحث في الوبّ هذا الموقع." email_domains_blacklist: "قائمة pipe-delimited المجالات البريد الإلكتروني الذي لا يسمح للمستخدمين تسجيل حسابات مع. مثال: mailinator.com | trashmail.net" email_domains_whitelist: "قائمة pipe-delimited من مجالات البريد الإلكتروني التي يجب على المستخدمين تسجيل حسابات مع. تحذير: لن يسمح للمستخدمين مع مجالات البريد الإلكتروني الأخرى غير المذكورة هنا!" - forgot_password_strict: "لا تخبر المستخدمين بوجود الحساب عند استخدامهم حوار نسيان كلمة السّرّ." + hide_email_address_taken: "لا تخبر المستخدمين بوجود الحساب عند استخدامهم حوار نسيان كلمة السّرّ." log_out_strict: "عند الخروج، اخرج من كلّ جلسات المستخدم في كلّ الأجهزة" version_checks: "Ping ديسكورس مركزا لتحديثات الإصدار وإظهار الرسائل النسخة الجديدة على لوحة القيادة / مسؤول" new_version_emails: "إرسال بريد إلكتروني إلى عنوان contact_email عندما نسخة جديدة من ديسكورس هو متاح." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 868d8a4888..aba151f27a 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -992,7 +992,7 @@ de: allow_index_in_robots_txt: "Suchmaschinen mittels der robots.txt Datei erlauben, die Site zu indizieren." email_domains_blacklist: "Eine durch senkrechte Striche getrennte Liste von E-Mail-Domains, die für die Registrierung neuer Konten nicht verwendet werden dürfen. Beispiel: mailinator.com|trashmail.net" email_domains_whitelist: "Eine durch senkrechte Striche getrennte Liste von E-Mail-Domains, die für die Registrierung neuer Konten verwendet werden können. ACHTUNG: Benutzer mit E-Mail-Adressen anderer Domains werden nicht zugelassen!" - forgot_password_strict: "Benutzer nicht informieren, ob ein Konto existiert, wenn sie den Passwort vergessen-Dialog verwenden." + hide_email_address_taken: "Benutzer nicht informieren, ob ein Konto existiert, wenn sie den Passwort vergessen-Dialog verwenden." log_out_strict: "Beim Abmelden ALLE Sitzungen des Benutzers auf allen Geräten beenden" version_checks: "Kontaktiere den Discourse Hub zur Überprüfung auf neue Versionen und zeige Benachrichtigungen über neue Versionen auf der Administratorkonsole an." new_version_emails: "Sende eine E-Mail an die contact_email Adresse wenn eine neue Version von Discourse verfügbar ist." diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index bd947e7d5d..2b6a86f7ce 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -1007,7 +1007,7 @@ el: allow_index_in_robots_txt: "Καθόρισε στο robots.txt ότι για αυτή την ιστοσελίδα επιτρέπεται να δημιουργείται κατάλογος περιεχομένων από τις μηχανές αναζήτησης." email_domains_blacklist: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες δεν μπορούν να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. Πχ: mailinator.com|trashmail.net" email_domains_whitelist: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες ΘΑ ΠΡΕΠΕΙ να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. ΠΡΟΣΟΧΗ: οι χρήστες με διευθύνσεις email οι οποίες δεν βρίσκονται σε αυτή τη λίστα δεν θα μπορούν να δημιουργήσουν λογαριασμό." - forgot_password_strict: "Να μην ενημερώνονται οι χρήστες για την ύπαρξη ενός λογαριασμού όταν χρησιμοποιούν την λειτουργία ανάκτησης κωδικού πρόσβασης." + hide_email_address_taken: "Να μην ενημερώνονται οι χρήστες για την ύπαρξη ενός λογαριασμού όταν χρησιμοποιούν την λειτουργία ανάκτησης κωδικού πρόσβασης." log_out_strict: "Όταν αποσυνδεθείτε, ΟΛΕΣ οι δραστηριότητες σας σε ΟΛΕΣ τις συσκευές θα αποσυνδεθούν" version_checks: "Έλεγξε το Hub του Discourse για αναβαθμίσεις και δείξε μηνύματα για νέες ενημερώσεις στην σελίδα διαχείρισης. " new_version_emails: "Αποστολή email στην contact_email διεύθυνση όταν μια νέα έκδοση του Discourse είναι διαθέσιμη." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d28b259caa..9c49b2588a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1097,7 +1097,7 @@ en: allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines." email_domains_blacklist: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Example: mailinator.com|trashmail.net" email_domains_whitelist: "A pipe-delimited list of email domains that users MUST register accounts with. WARNING: Users with email domains other than those listed will not be allowed!" - forgot_password_strict: "Don't inform users of an account's existence when they use the forgot password dialog." + hide_email_address_taken: "Don't inform users that an account exists with a given email address during signup and from the forgot password form." log_out_strict: "When logging out, log out ALL sessions for the user on all devices" version_checks: "Ping the Discourse Hub for version updates and show new version messages on the /admin dashboard" new_version_emails: "Send an email to the contact_email address when a new version of Discourse is available." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 3292aa4679..a1a7d20f2b 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -971,7 +971,7 @@ es: allow_index_in_robots_txt: "Especificar en robots.txt que este sitio puede ser indexado por los motores de búsqueda web." email_domains_blacklist: "Una lista de dominios de correo electrónico con los que los usuarios no se podrán registrar. Ejemplo: mailinator.com|trashmail.net" email_domains_whitelist: "Una lista de dominios de email con los que los usuarios DEBERÁN registrar sus cuentas. AVISO: ¡los usuarios con un email con diferente dominio a los listados no estarán permitidos!" - forgot_password_strict: "No informar a los usuarios de la existencia de una cuenta cuando utilicen el diálogo de pérdida de contraseña." + hide_email_address_taken: "No informar a los usuarios de la existencia de una cuenta cuando utilicen el diálogo de pérdida de contraseña." log_out_strict: "Al cerrar sesión, cierra TODAS las sesiones del usuario en todos los dispositivos" version_checks: "Ping el 'Discourse Hub' para actualizaciones de versión y mostrar mensajes del número de versión en el dashboard /admin" new_version_emails: "Enviar un email a la dirección contact_email cuando esté disponible una nueva versión de Discourse." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 037617e819..cff8e54b35 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -929,7 +929,7 @@ fa_IR: allow_index_in_robots_txt: "در robots.txt که به این سایت اجازه دهید که در موتور‌های جستجو ایندکس شود." email_domains_blacklist: "لیست pipe-delimit دامنه های ایمیل که کاربران اجازه ندارند با آن حساب کاربری ثبت‌نام کنند. برای مثال: mailinator.com|trashmail.net" email_domains_whitelist: "لیست pipe-delimit دامنه های ایمیل که کاربران اجازه باید با آن حساب کاربری ثبت نام کنند. اخطار: کاربرانی با دامنه‌های ایمیلی به غیر از آن‌هایی که در لیست هستند اجازه ندارند. " - forgot_password_strict: "آگاه نکردن کاربران از وجود حساب کاربری در صفحه فراموشی روز عبور" + hide_email_address_taken: "آگاه نکردن کاربران از وجود حساب کاربری در صفحه فراموشی روز عبور" log_out_strict: "وقتی از سیستم خارج می شود، کاربر را از تمام session‌ها بر روی تمام دستگاه‌ها خارج کن " version_checks: "Discourse Hub را پینگ کن برای نسخه بروزرسانی و پیام نسخه جدید را صفحه آمار ادمین نشان بده." new_version_emails: "ارسال ایمیل به contact_email address وقتی نسخه جدید Discourse موجود است. " diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 38389f578f..7b810f186e 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -991,7 +991,7 @@ fi: allow_index_in_robots_txt: "Määrittele robots.txt-tiedostossa, että hakukoneet saavat luetteloida sivuston." email_domains_blacklist: "Pystyviivalla eroteltu lista sähköposti-verkkotunnuksista, joista käyttäjät eivät voi luoda tiliä. Esimerkiksi mailinator.com|trashmail.net" email_domains_whitelist: "Pystyviivalla eroteltu lista sähköposti-verkkotunnuksista, joista käyttäjien pitää luoda tilinsä. VAROITUS: Käyttäjiä, joiden sähköpostiosoite on muusta verkkotunnuksesta ei sallita!" - forgot_password_strict: "Älä paljasta tilin olemassaoloa unohtuneen salasanan kyselyssä." + hide_email_address_taken: "Älä paljasta tilin olemassaoloa unohtuneen salasanan kyselyssä." log_out_strict: "Kun kirjaudutaan ulos, kirjaa käyttäjä ulos kaikilta laitteilta" version_checks: "Pingaa Discourse Hubia päivityksistä ja näytä viesti /admin hallintapaneelissa kun uusi versio on saatavilla" new_version_emails: "Lähetä sähköposti contact_email osoitteeseen kun uusi versio Discoursesta on saatavilla." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index a6d9f8a8fa..842705b220 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -984,7 +984,7 @@ fr: allow_index_in_robots_txt: "Préciser dans robots.txt que le site est autorisé à être indexé par les robots des moteurs de recherche." email_domains_blacklist: "Liste des domaines de courriel qui ne sont pas autorisés lors de la création de compte, délimitée par des pipes. Exemple : mailinator.com|trashmail.net" email_domains_whitelist: "Liste des domaines de courriel avec lesquelles les utilisateurs DOIVENT s'enregistrer, délimités par un pipe. ATTENTION : les utilisateurs ayant une adresse de courriel sur un autre domaine ne pourront pas s'enregistrer." - forgot_password_strict: "Ne pas mentionner l'existence d'un compte utilisateur quand un utilisateur utilise le formulaire d'oubli de mot de passe." + hide_email_address_taken: "Ne pas mentionner l'existence d'un compte utilisateur quand un utilisateur utilise le formulaire d'oubli de mot de passe." log_out_strict: "Lors de la déconnexion, déconnecter TOUTES les sessions pour l'utilisateur sur tous les appareils" version_checks: "Ping les serveurs de Discourse afin d'obtenir les mises à jours et affiche les nouveaux messages d'information dans le tableau de bord /admin" new_version_emails: "Envoyer un courriel à contact_email quand une nouvelle version de Discourse est disponible." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 9f0909d571..c3ca363b93 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -974,7 +974,7 @@ he: allow_index_in_robots_txt: "פרטו ב-robots.txt שלאתר זה מותר להיות מאונדקס על ידי מנועי חיפוש." email_domains_blacklist: "רשימה מופרדת בצינור (pipe) של דומיינים של אימייל אשר מהם משתמשים לא מורשים לרשום חשבונות. למשל: mailinator.com|trashmail.net" email_domains_whitelist: "רשימה מופרדת בצינור (pipe) אשר ר-ק ממנה משתמשים יכולים לרשום חשבונות. א-ז-ה-ר-ה: משתמשים עם אימיילים מדומיינים אחרים לא יורשו!" - forgot_password_strict: "אל תיידעו משתמשים בנוגע לקיום חשבון כשהם משתמשים באפשרות של ״שכחתי סיסמה״." + hide_email_address_taken: "אל תיידעו משתמשים בנוגע לקיום חשבון כשהם משתמשים באפשרות של ״שכחתי סיסמה״." log_out_strict: "בהתנתקות, נתקו את כל ההפעלות של המשתמש/ת בכל המכשירים" version_checks: "שלחו פינג להאב של Discourse לעדכוני גרסה וכדי להציג מסרים אודות גרסאות בלוח התצוגה ב /admin" new_version_emails: "שלחו דוא\"ל לכתובת של contact_email כשגרסה חדשה של Discourse זמינה." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index a9ca197a7f..2973c69013 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -1000,7 +1000,7 @@ it: allow_index_in_robots_txt: "Specifica nel file robots.txt che questo sito permette l'indicizzazione da parte dei motori di ricerca." email_domains_blacklist: "Una lista di domini email separati dal carattere pipe \"|\" con cui gli utenti non possono registrare un account. Ad esempio: mailinator.com/trashmail.net" email_domains_whitelist: "Una lista di domini email delimitati da carattere pipe (|) che gli utenti DEVONO usare per poter registrare i propri account. ATTENZIONE: gli utenti con domini email differenti da questi non saranno accettati!" - forgot_password_strict: "Non informare gli utenti dell'esistenza o meno dell'account quando richiamano la funzione per la password dimenticata." + hide_email_address_taken: "Non informare gli utenti dell'esistenza o meno dell'account quando richiamano la funzione per la password dimenticata." log_out_strict: "Quando ci si disconnette, esci da TUTTE le sessioni dell'utente su tutti i dispositivi" version_checks: "Verifica su Discourse Hub l'esistenza di aggiornamenti e mostra i messaggi per le nuove versioni nel cruscotto /admin" new_version_emails: "Invia un'email all'indirizzo contact_email quando è disponibile una nuova versione di Discourse." diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 942e656ce2..196825c24d 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -925,7 +925,7 @@ ko: allow_index_in_robots_txt: "이 사이트가 검색엔진에 의해 인덱스되는 것을 허용합니다.(robots.txt 수정)" email_domains_blacklist: "가입 금지된 이메일 도메인 목록, 파이프 기호로 구분. 예: mailinator.com|trashmail.net" email_domains_whitelist: "가입하려면 반드시 사용해야하는 이메일 도메인 목록, 파이프 기호로 구분. 경고: 이 목록외의 도메인으로는 가입이 안됩니다!" - forgot_password_strict: "비밀번호 찾기 창에서 사용자 계정의 존재 여부를 알리지 않음." + hide_email_address_taken: "비밀번호 찾기 창에서 사용자 계정의 존재 여부를 알리지 않음." log_out_strict: "로그아웃 할때, 모든 장치에서 다같이 로그아웃" version_checks: "Dicousre Hub에 ping을 날려 버전 업데이트와 새 버전 알림을 /admin 대시보드에 보이게 합니다." new_version_emails: "사용가능한 새로운 업데이트가 있으면 등록된 contact_email 주소로 메일을 발송하여 알려줍니다." diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 1f036294f7..2ed4faeb49 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -1059,7 +1059,7 @@ pl_PL: allow_index_in_robots_txt: "Określ w robots.txt, że ta strona może być indeksowana przez silniki wyszukiwania." email_domains_blacklist: "Lista rozdzielonych pionową kreską domen e-mail, z których użytkownicy nie mogą się rejestrować. Przykład: mailinator.com|trashmail.net" email_domains_whitelist: "Lista rozdzielonych pionową kreską domen e-mail, z których użytkownicy MUSZĄ się rejestrować. UWAGA: Użytkownicy z domenami e-mail innymi niż wypisane nie będą dopuszczeni!" - forgot_password_strict: "Nie informuj użytkowników o istnieniu konta kiedy używają dialogu zapomnianego hasła." + hide_email_address_taken: "Nie informuj użytkowników o istnieniu konta kiedy używają dialogu zapomnianego hasła." log_out_strict: "Po wylogowaniu wyloguj WSZYSTKIE sesje użytkownika na wszystkich urządzeniach." version_checks: "Odpytuj Discourse Hub o aktualizacje i wyświetlaj wiadomości o nowej wersji w panelu /admin" new_version_emails: "Wyślij email na adres contact_email, kiedy nowa wersja Discourse będzie dostępna." diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index b6fcdfe8d6..ace016fe07 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -897,7 +897,7 @@ pt: allow_index_in_robots_txt: "Especificar em robots.txt que este sítio permite ser indexado pelos motores de pesquisa." email_domains_blacklist: "Lista de domínios de email que os utilizadores não podem usar para registo de contas. Exemplo: mailinator.com|trashmail.net" email_domains_whitelist: "Lista de domínios de email que os utilizadores DEVEM usar para registar contas. AVISO: Utilizadores com domínios de email diferentes dos listados não serão permitidos!" - forgot_password_strict: "Não informar utilizadores da existência de uma conta quando estes usam o diálogo de palavra-passe esquecida. " + hide_email_address_taken: "Não informar utilizadores da existência de uma conta quando estes usam o diálogo de palavra-passe esquecida. " log_out_strict: "Ao terminar sessão, saia de TODAS as sessões do utilizador em todos dispositivos" version_checks: "Fazer o ping do Discourse Hub para atualização de versões e mostrar mensagens sobre novas versões no painel de administração" new_version_emails: "Enviar um email para o endereço 'contact_email' quando uma nova versão do Discourse estiver disponível." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index a31661eacc..252743bddc 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -885,7 +885,7 @@ pt_BR: allow_index_in_robots_txt: "Especificar no robots.txt que este site é permitido de ser indexado por sistemas de busca na web." email_domains_blacklist: "Lista delimitada por barras (|) de domínios de email que não são permitidos registros de contas. Exemplo: mailinator.com|trashmail.net" email_domains_whitelist: "Lista separada por barra (|) de domínios de email que usuários DEVEM usar para registrar contas. CUIDADO: Usuário com domínio de email diferentes da lista não serão permitidos!" - forgot_password_strict: "Não informar os usuários da existência de uma conta quando eles usam o diálogo de esquecimento de senha." + hide_email_address_taken: "Não informar os usuários da existência de uma conta quando eles usam o diálogo de esquecimento de senha." log_out_strict: "Quando deslogando, deslogar TODAS as sessões do usuário em todos os dispositivos" version_checks: "Pingar Discourse Hub para atualizações de versão e exibir mensagens de versão no Painel em /admin" new_version_emails: "Enviar um email para o endereço contact_email quando uma nova versão do Discourse estiver disponível." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index f611b19cf6..aa71025a2f 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -911,7 +911,7 @@ ro: allow_index_in_robots_txt: "Specifică în robots.txt că acest site poate fi indexat de motoarele de căutare web." email_domains_blacklist: "O listă de domenii de email separate cu simbolul | (pipe) ale căror utilizatori nu au permisiunea să înregistreze conturi. Exemplu: mailinator.com|trashmail.net" email_domains_whitelist: "O listă de domenii de email (separate cu simbolul | (pipe)) cu care utilizatorii TREBUIE să se înregistreze. ATENȚIE: utilizatorii cu alte domenii de email decât cele listate nu vor avea permisiunea să se înregistreze." - forgot_password_strict: "Nu informa utilizatorii despre existența unui cont atunci când folosesc dialogul pentru parolă uitată." + hide_email_address_taken: "Nu informa utilizatorii despre existența unui cont atunci când folosesc dialogul pentru parolă uitată." log_out_strict: "La ieșire, închide TOATE sesiunile pentru utilizator, pe toate dispozitivele." version_checks: "Verifică Hub-ul Discourse pentru actualizări și arată notificările de versiuni noi pe spațiul de lucru /admin ." new_version_emails: "Trimite un email la adresa contact_email când o nouă versiune de Discourse este disponibilă." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 1f94f64aea..8cc2c66904 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -848,7 +848,7 @@ sv: allow_index_in_robots_txt: "Specificera i robots.txt att den här webbplatsen tillåter att bli indexerad av sökmotorer. " email_domains_blacklist: "En pipe-avgränsad lista av alla e-postdomän som användare inte tillåts registrera konton med. Exempel: mailinator.com, trashmail.net" email_domains_whitelist: "En pipe-avgränsad lista av alla e-postdomän som användare MÅSTE registrera konton med. VARNING: Användare med e-postdomän som inte finns på listan kommer inte att tillåtas!" - forgot_password_strict: "Informera inte användare om ett kontos existens när de försöker använda dialogen vid bortglömt lösenord. " + hide_email_address_taken: "Informera inte användare om ett kontos existens när de försöker använda dialogen vid bortglömt lösenord. " log_out_strict: "Vid utloggning, logga ut ALLA sessioner för den användaren på alla apparater" version_checks: "Pinga Discourse Hubben för versionsuppdatering och visa nya versionsmeddelanden på /admin översiktspanelen" new_version_emails: "Skicka ett mejl till contact_email när en ny version av Discourse finns tillgängligt." diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 6fb71b90ff..fbd877771a 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -737,7 +737,7 @@ tr_TR: allow_index_in_robots_txt: "robots.txt dosyasında bu sitenin arama motorları tarafından dizinlenmesine izin verildiğini belirt." email_domains_blacklist: "Kullanıcıların kayıt olurken kullanamayacağı e-posta alan adlarının, dikey çizgilerle ayrıştırılmış listesi. Örneğin: mailinator.com|trashmail.net" email_domains_whitelist: "Kullanıcıların kayıt olurken kullanmak ZORUNDA olduğu e-posta alan adlarının, dikey çizgilerle ayrıştırılmış listesi. UYARI: Bu listede yer almayan e-posta alan adları kabul edilmeyecektir!" - forgot_password_strict: "Parola sıfırlama kullanıldığında kullanıcıyı hesabın varlığı ile ilgili olarak bilgilendirme." + hide_email_address_taken: "Parola sıfırlama kullanıldığında kullanıcıyı hesabın varlığı ile ilgili olarak bilgilendirme." log_out_strict: "Çıkış yapılırken, kullanıcının tüm cihazlardaki TÜM seanslarını sonlandır" version_checks: "Discourse Hub'a sürüm güncellemeleri için haber yolla ve yeni versiyon iletilerine /admin gösterge panelinde yer ver" new_version_emails: "Discourse'un yeni sürümü çıktığında contact_email adresine e-posta gönder." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 1211bd3a0d..c0784b48d3 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -953,7 +953,7 @@ zh_CN: allow_index_in_robots_txt: "在 robots.txt 中详细指出这个站点允许被网页搜索引擎检索。" email_domains_blacklist: "用管道符“|”分隔的邮箱域名黑名单列表,其中的域名将不能用来注册账户,例如:mailinator.com|trashmail.net" email_domains_whitelist: "用管道符“|”分隔的电子邮箱域名的列表,用户必须使用这些邮箱域名注册。警告:用户使用不包含在这个列表里的邮箱域名,将无法成功注册。" - forgot_password_strict: "用户找回密码时不提示帐户是否存在。" + hide_email_address_taken: "用户找回密码时不提示帐户是否存在。" log_out_strict: "退出时,退出用户所有设备的会话" version_checks: "访问 Discourse Hub 来检查版本更新,并在管理面板 /admin 显示新版本信息" new_version_emails: "当新版本发布时,发送一封邮件至 contact_email 设置的地址。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 791e179028..353eaf8fdb 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -890,7 +890,7 @@ zh_TW: allow_index_in_robots_txt: "在 robots.txt 中記錄這個網站允許被搜尋引擎索引的部分" email_domains_blacklist: "用管道符“|”分隔的郵箱域名黑名單列表,其中的域名將不能用來註冊賬戶,例如:mailinator.com|trashmail.net" email_domains_whitelist: "用管道符“|”分隔的電子郵箱域名的列表,用戶必須使用這些郵箱域名註冊。警告:用戶使用不包含在這個列表裡的郵箱域名,將無法成功註冊。" - forgot_password_strict: "用戶找回密碼時不提示帳戶是否存在。" + hide_email_address_taken: "用戶找回密碼時不提示帳戶是否存在。" log_out_strict: "登出時,登出用戶所有設備上的所有時段" version_checks: "訪問 Discourse Hub 來檢查版本更新,並在管理面板 /admin 顯示新版本訊息" new_version_emails: "當新版本發佈時,將會發送一封新的 EMail 至 contact_email 設定的位址" diff --git a/config/site_settings.yml b/config/site_settings.yml index 0331d5a520..3e42c07db8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -321,7 +321,7 @@ login: email_domains_whitelist: default: '' type: list - forgot_password_strict: false + hide_email_address_taken: false log_out_strict: true pending_users_reminder_delay: min: -1 diff --git a/db/migrate/20171003180951_rename_forgot_password_strict_setting.rb b/db/migrate/20171003180951_rename_forgot_password_strict_setting.rb new file mode 100644 index 0000000000..0a9945bb96 --- /dev/null +++ b/db/migrate/20171003180951_rename_forgot_password_strict_setting.rb @@ -0,0 +1,9 @@ +class RenameForgotPasswordStrictSetting < ActiveRecord::Migration[5.1] + def up + execute "UPDATE site_settings SET name = 'hide_email_address_taken' WHERE name = 'forgot_password_strict'" + end + + def down + execute "UPDATE site_settings SET name = 'forgot_password_strict' WHERE name = 'hide_email_address_taken'" + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index fcd265fef3..258d788f9b 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -625,16 +625,16 @@ describe UsersController do context 'users already exists with given email' do let!(:existing) { Fabricate(:user, email: post_user_params[:email]) } - it 'returns an error if forgot_password_strict is disabled' do - SiteSetting.forgot_password_strict = false + it 'returns an error if hide_email_address_taken is disabled' do + SiteSetting.hide_email_address_taken = false post_user json = JSON.parse(response.body) expect(json['success']).to eq(false) expect(json['message']).to be_present end - it 'returns success if forgot_password_strict is enabled' do - SiteSetting.forgot_password_strict = true + it 'returns success if hide_email_address_taken is enabled' do + SiteSetting.hide_email_address_taken = true expect { post_user }.to_not change { User.count } From 2ce6e0bb07d29f8e75c63640b0b28381b1390d5b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 3 Oct 2017 15:38:45 -0400 Subject: [PATCH 067/108] UX: Perform icon replacements before calling icon renderer --- .../javascripts/discourse-common/lib/icon-library.js.es6 | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index da6ac057d3..4037efeed1 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -40,7 +40,7 @@ export function renderIcon(renderType, id, params) { let rendererForType = renderer[renderType]; if (rendererForType) { - let result = rendererForType(id, params || {}); + let result = rendererForType(REPLACEMENTS[id] || id, params || {}); if (result) { return result; } @@ -80,8 +80,6 @@ registerIconRenderer({ name: 'font-awesome', string(id, params) { - id = REPLACEMENTS[id] || id; - let tagName = params.tagName || 'i'; let html = `<${tagName} class='${faClasses(id, params)}'`; if (params.title) { html += ` title='${I18n.t(params.title)}'`; } @@ -94,8 +92,6 @@ registerIconRenderer({ }, node(id, params) { - id = REPLACEMENTS[id] || id; - let tagName = params.tagName || 'i'; const properties = { From ab12c40e76b7c29a2fae49a2edad459f3c0ced47 Mon Sep 17 00:00:00 2001 From: Jay Pfaffman Date: Tue, 3 Oct 2017 14:09:32 -0700 Subject: [PATCH 068/108] Tweak error messages for restore --- script/discourse | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/script/discourse b/script/discourse index 9013c87b79..c5d16d36e3 100755 --- a/script/discourse +++ b/script/discourse @@ -87,11 +87,17 @@ class DiscourseCLI < Thor desc "restore", "Restore a Discourse backup" def restore(filename = nil) + if File.exist?('/usr/local/bin/discourse') + discourse = 'discourse' + else + discourse = './script/discourse' + end + if !filename puts "You must provide a filename to restore. Did you mean one of the following?\n\n" Dir["public/backups/default/*"].each do |f| - puts "discourse restore #{File.basename(f)}" + puts "#{discourse} restore #{File.basename(f)}" end return @@ -110,7 +116,8 @@ class DiscourseCLI < Thor puts '', 'The filename argument was missing.', '' usage rescue BackupRestore::RestoreDisabledError - puts '', 'Restores are not allowed.', 'An admin needs to set allow_restore to true in the site settings before restores can be run.', '' + puts '', 'Restores are not allowed.', 'An admin needs to set allow_restore to true in the site settings before restores can be run.' + puts "Enable now with", '', "#{discourse} enable_restore", '' puts 'Restore cancelled.', '' end From 9ff1c23a38cea1bbee247de3e842a0624e1460cd Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 4 Oct 2017 00:01:33 +0200 Subject: [PATCH 069/108] fix typo --- lib/email/processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/email/processor.rb b/lib/email/processor.rb index b57fb9a003..2e94811256 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -50,7 +50,7 @@ module Email when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found when Email::Receiver::TopicClosedError then :email_reject_topic_closed - when Email::Receiver::InvalidPost then :email_reject_invalid_pos + when Email::Receiver::InvalidPost then :email_reject_invalid_post when Email::Receiver::UnsubscribeNotAllowed then :email_reject_invalid_post when ActiveRecord::Rollback then :email_reject_invalid_post when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action From 4ee2fcd3d56c964f1032d893a7b3a708a3bda349 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Oct 2017 10:47:02 +1100 Subject: [PATCH 070/108] correct flaky spec --- spec/components/scheduler/manager_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/scheduler/manager_spec.rb b/spec/components/scheduler/manager_spec.rb index 0a229eed32..191e0f5c9a 100644 --- a/spec/components/scheduler/manager_spec.rb +++ b/spec/components/scheduler/manager_spec.rb @@ -77,7 +77,7 @@ describe Scheduler::Manager do ActiveRecord::Base.connection_pool.connections.reject { |c| c.in_use? }.each do |c| ActiveRecord::Base.connection_pool.remove(c) end - expect(ActiveRecord::Base.connection_pool.connections.length).to eq(1) + expect(ActiveRecord::Base.connection_pool.connections.length).to (be <= 1) on_thread_mismatch = lambda do current = Thread.list.map { |t| t.object_id } From 0342324b4721ca0d152125a6c583cddb218c0c79 Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Tue, 3 Oct 2017 20:47:53 -0400 Subject: [PATCH 071/108] FEATURE: support regex in rake post:remap (#5201) --- app/models/post.rb | 10 +++++++ lib/tasks/posts.rake | 58 +++++++++++++++++++++++++--------------- spec/tasks/posts_spec.rb | 30 ++++++++++++++++++++- 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/app/models/post.rb b/app/models/post.rb index 992e3a83a9..a70c69a915 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -91,6 +91,16 @@ class Post < ActiveRecord::Base q.order('posts.created_at ASC') } + scope :raw_match, -> (pattern, type = 'string') { + type = type&.downcase + + case type + when 'string' + where('raw ILIKE ?', "%#{pattern}%") + when 'regex' + where('raw ~ ?', pattern) + end + } delegate :username, to: :user diff --git a/lib/tasks/posts.rake b/lib/tasks/posts.rake index 6814c5c093..8a4bc69b6a 100644 --- a/lib/tasks/posts.rake +++ b/lib/tasks/posts.rake @@ -42,23 +42,18 @@ end desc 'Rebake all posts matching string/regex and optionally delay the loop' task 'posts:rebake_match', [:pattern, :type, :delay] => [:environment] do |_, args| + args.with_defaults(type: 'string') pattern = args[:pattern] - type = args[:type] - type = type.downcase if type - delay = args[:delay].to_i if args[:delay] + type = args[:type]&.downcase + delay = args[:delay]&.to_i + if !pattern puts "ERROR: Expecting rake posts:rebake_match[pattern,type,delay]" exit 1 elsif delay && delay < 1 puts "ERROR: delay parameter should be an integer and greater than 0" exit 1 - end - - if type == "regex" - search = Post.where("raw ~ ?", pattern) - elsif type == "string" || !type - search = Post.where("raw ILIKE ?", "%#{pattern}%") - else + elsif type != 'string' && type != 'regex' puts "ERROR: Expecting rake posts:rebake_match[pattern,type] where type is string or regex" exit 1 end @@ -66,7 +61,7 @@ task 'posts:rebake_match', [:pattern, :type, :delay] => [:environment] do |_, ar rebaked = 0 total = search.count - search.find_each do |post| + Post.raw_match(pattern, type).find_each do |post| rebake_post(post) print_status(rebaked += 1, total) sleep(delay) if delay @@ -130,11 +125,15 @@ task 'posts:normalize_code' => :environment do puts "#{i} posts normalized!" end -def remap_posts(find, replace = "") +def remap_posts(find, type, replace = "") i = 0 - Post.where("raw LIKE ?", "%#{find}%").each do |p| - new_raw = p.raw.dup - new_raw = new_raw.gsub!(/#{Regexp.escape(find)}/, replace) || new_raw + + Post.raw_match(find, type).find_each do |p| + new_raw = + case type + when 'string' then p.raw.gsub(/#{Regexp.escape(find)}/, replace) + when 'regex' then p.raw.gsub(/#{find}/, replace) + end if new_raw != p.raw p.revise(Discourse.system_user, { raw: new_raw }, bypass_bump: true, skip_revision: true) @@ -142,42 +141,59 @@ def remap_posts(find, replace = "") i += 1 end end + i end desc 'Remap all posts matching specific string' -task 'posts:remap', [:find, :replace] => [:environment] do |_, args| +task 'posts:remap', [:find, :replace, :type] => [:environment] do |_, args| + require 'highline/import' + args.with_defaults(type: 'string') find = args[:find] replace = args[:replace] + type = args[:type]&.downcase + if !find puts "ERROR: Expecting rake posts:remap['find','replace']" exit 1 elsif !replace puts "ERROR: Expecting rake posts:remap['find','replace']. Want to delete a word/string instead? Try rake posts:delete_word['word-to-delete']" exit 1 + elsif type != 'string' && type != 'regex' + puts "ERROR: Expecting rake posts:delete_word[pattern, type] where type is string or regex" + exit 1 + else + confirm_replace = ask("Are you sure you want to replace all #{type} occurrences of '#{find}' with '#{replace}'? (Y/n)") + exit 1 unless (confirm_replace == "" || confirm_replace.downcase == 'y') end puts "Remapping" - total = remap_posts(find, replace) + total = remap_posts(find, type, replace) puts "", "#{total} posts remapped!", "" end desc 'Delete occurrence of a word/string' -task 'posts:delete_word', [:find] => [:environment] do |_, args| +task 'posts:delete_word', [:find, :type] => [:environment] do |_, args| require 'highline/import' + args.with_defaults(type: 'string') find = args[:find] + type = args[:type]&.downcase + if !find puts "ERROR: Expecting rake posts:delete_word['word-to-delete']" exit 1 + elsif type != 'string' && type != 'regex' + puts "ERROR: Expecting rake posts:delete_word[pattern, type] where type is string or regex" + exit 1 else - confirm_replace = ask("Are you sure you want to remove all occurrences of '#{find}'? (Y/n) ") - exit 1 unless (confirm_replace == "" || confirm_replace.downcase == 'y') + confirm_delete = ask("Are you sure you want to remove all #{type} occurrences of '#{find}'? (Y/n)") + exit 1 unless (confirm_delete == "" || confirm_delete.downcase == 'y') end puts "Processing" - total = remap_posts(find) + total = remap_posts(find, type) puts "", "#{total} posts updated!", "" end diff --git a/spec/tasks/posts_spec.rb b/spec/tasks/posts_spec.rb index f69a3bbf68..7f3e307f9e 100644 --- a/spec/tasks/posts_spec.rb +++ b/spec/tasks/posts_spec.rb @@ -1,18 +1,46 @@ require 'rails_helper' +require 'highline/import' +require 'highline/simulate' RSpec.describe "Post rake tasks" do before do + Rake::Task.clear Discourse::Application.load_tasks IO.any_instance.stubs(:puts) end describe 'remap' do + let!(:tricky_post) { Fabricate(:post, raw: 'Today ^Today') } + it 'should remap posts' do post = Fabricate(:post, raw: "The quick brown fox jumps over the lazy dog") - Rake::Task['posts:remap'].invoke("brown", "red") + HighLine::Simulate.with('y') do + Rake::Task['posts:remap'].invoke("brown", "red") + end + post.reload expect(post.raw).to eq('The quick red fox jumps over the lazy dog') end + + context 'when type == string' do + it 'remaps input as string' do + HighLine::Simulate.with('y') do + Rake::Task['posts:remap'].invoke('^Today', 'Yesterday', 'string') + end + + expect(tricky_post.reload.raw).to eq('Today Yesterday') + end + end + + context 'when type == regex' do + it 'remaps input as regex' do + HighLine::Simulate.with('y') do + Rake::Task['posts:remap'].invoke('^Today', 'Yesterday', 'regex') + end + + expect(tricky_post.reload.raw).to eq('Yesterday ^Today') + end + end end end From a4d4db4f0c1d2bd841611abbd9627f9696e215a3 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Oct 2017 14:22:23 +1100 Subject: [PATCH 072/108] PERF: code not correctly caching git commands Every check for Discourse version could result in shelling out. --- config/unicorn.conf.rb | 5 ++++ lib/discourse.rb | 54 ++++++++++++++++++++++++------------------ 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index e0d0894cec..decabee44e 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -85,6 +85,11 @@ before_fork do |server, worker| end end + # preload discourse version + Discourse.git_version + Discourse.git_branch + Discourse.full_version + # get rid of rubbish so we don't share it GC.start diff --git a/lib/discourse.rb b/lib/discourse.rb index dfcc1a90bd..f09037cb43 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -327,42 +327,50 @@ module Discourse end end - def self.git_version - return $git_version if $git_version + def self.ensure_version_file_loaded + unless @version_file_loaded + version_file = "#{Rails.root}/config/version.rb" + require version_file if File.exists?(version_file) + @version_file_loaded = true + end + end - git_cmd = 'git rev-parse HEAD' - self.load_version_or_git(git_cmd, Discourse::VERSION::STRING) { $git_version } + def self.git_version + ensure_version_file_loaded + $git_version ||= + begin + git_cmd = 'git rev-parse HEAD' + self.try_git(git_cmd, Discourse::VERSION::STRING) + end end def self.git_branch - return $git_branch if $git_branch - git_cmd = 'git rev-parse --abbrev-ref HEAD' - self.load_version_or_git(git_cmd, 'unknown') { $git_branch } + ensure_version_file_loaded + $git_branch ||= + begin + git_cmd = 'git rev-parse --abbrev-ref HEAD' + self.try_git(git_cmd, 'unknown') + end end def self.full_version - return $full_version if $full_version - git_cmd = 'git describe --dirty --match "v[0-9]*"' - self.load_version_or_git(git_cmd, 'unknown') { $full_version } + ensure_version_file_loaded + $full_version ||= + begin + git_cmd = 'git describe --dirty --match "v[0-9]*"' + self.try_git(git_cmd, 'unknown') + end end - def self.load_version_or_git(git_cmd, default_value) - version_file = "#{Rails.root}/config/version.rb" + def self.try_git(git_cmd, default_value) version_value = false - if File.exists?(version_file) - require version_file - version_value = yield + begin + version_value = `#{git_cmd}`.strip + rescue + version_value = default_value end - # file does not exist or does not define the expected global variable - unless version_value - begin - version_value = `#{git_cmd}`.strip - rescue - version_value = default_value - end - end if version_value.empty? version_value = default_value end From 14310d2eee204e887239b8b7a3377f49e815ff53 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Oct 2017 15:04:42 +1100 Subject: [PATCH 073/108] UX: title in JS must match title on Server Corrects title flashing with incorrect value on front page reloads --- app/views/list/list.erb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/list/list.erb b/app/views/list/list.erb index d8679ae7bd..6920188fbf 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -104,6 +104,8 @@ <% content_for :title do %><%= @title %><% end %> <% elsif @category %> <% content_for :title do %><%= @category.name %> - <%= SiteSetting.title %><% end %> -<% elsif params[:page] %> +<% elsif params[:page].to_i > 1 %> <% content_for :title do %><%=t 'page_num', num: params[:page].to_i + 1 %> - <%= SiteSetting.title %><% end %> +<% else %> + <% content_for :title do %><%= SiteSetting.title %><% end %> <% end %> From ebdf8d67185e6027e1929f4003a60ede7ee199eb Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Oct 2017 15:05:58 +1100 Subject: [PATCH 074/108] remove uneeded code --- app/views/list/list.erb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/list/list.erb b/app/views/list/list.erb index 6920188fbf..7d1179c746 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -106,6 +106,4 @@ <% content_for :title do %><%= @category.name %> - <%= SiteSetting.title %><% end %> <% elsif params[:page].to_i > 1 %> <% content_for :title do %><%=t 'page_num', num: params[:page].to_i + 1 %> - <%= SiteSetting.title %><% end %> -<% else %> - <% content_for :title do %><%= SiteSetting.title %><% end %> <% end %> From 58813550064d3ed5bb5a2ace1037f1cfd4eaed05 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 4 Oct 2017 15:59:16 +1100 Subject: [PATCH 075/108] remove uneeded assertion --- spec/components/scheduler/manager_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/components/scheduler/manager_spec.rb b/spec/components/scheduler/manager_spec.rb index 191e0f5c9a..6ae829c558 100644 --- a/spec/components/scheduler/manager_spec.rb +++ b/spec/components/scheduler/manager_spec.rb @@ -70,7 +70,6 @@ describe Scheduler::Manager do manager.remove(Testing::SuperLongJob) manager.remove(Testing::PerHostJob) $redis.flushall - expect(ActiveRecord::Base.connection_pool.connections.reject { |c| !c.in_use? }.length).to eq(1) # connections that are not in use must be removed # otherwise active record gets super confused From 1310181664e5c20204378f7b8bd3fa98f20483f3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 4 Oct 2017 16:31:40 +0800 Subject: [PATCH 076/108] FIX: Adding a public topic timer deletes a private topic timer. --- app/models/topic.rb | 5 +++-- spec/models/topic_spec.rb | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/models/topic.rb b/app/models/topic.rb index 86feb633a3..161d5ba361 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1022,8 +1022,9 @@ SQL def set_or_create_timer(status_type, time, by_user: nil, timezone_offset: 0, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id) return delete_topic_timer(status_type, by_user: by_user) if time.blank? - topic_timer_options = { topic: self } - topic_timer_options.merge!(user: by_user) unless TopicTimer.public_types[status_type] + public_topic_timer = !!TopicTimer.public_types[status_type] + topic_timer_options = { topic: self, public_type: public_topic_timer } + topic_timer_options.merge!(user: by_user) unless public_topic_timer topic_timer = TopicTimer.find_or_initialize_by(topic_timer_options) topic_timer.status_type = status_type diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index c152875acc..ce3ece7a4f 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1333,6 +1333,14 @@ describe Topic do }.to change { TopicTimer.count }.by(1) end + it 'should not be override when setting a public topic timer' do + reminder + + expect do + topic.set_or_create_timer(TopicTimer.types[:close], 3, by_user: reminder.user) + end.to change { TopicTimer.count }.by(1) + end + it "can update a user's existing record" do freeze_time now From abdb3348233764e84f9986e23f8190a95e99344f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 4 Oct 2017 11:07:59 -0400 Subject: [PATCH 077/108] UX: Allow for customization of the heart icon --- .../javascripts/discourse-common/lib/icon-library.js.es6 | 2 ++ app/assets/javascripts/discourse/widgets/post-menu.js.es6 | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 4037efeed1..0dcf4051cd 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -9,6 +9,8 @@ const REPLACEMENTS = { 'd-watching-first': 'dot-circle-o', 'd-drop-expanded': 'caret-down', 'd-drop-collapsed': 'caret-right', + 'd-unliked': 'heart', + 'd-liked': 'heart', 'notification.mentioned': "at", 'notification.group_mentioned': "at", 'notification.quoted': "quote-right", diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index deda3b167a..b36fdb21bb 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -35,10 +35,11 @@ registerButton('like', attrs => { const button = { action: 'like', - icon: 'heart', + icon: attrs.liked ? 'd-liked' : 'd-unliked', className }; + if (attrs.canToggleLike) { button.title = attrs.liked ? 'post.controls.undo_like' : 'post.controls.like'; } else if (attrs.liked) { @@ -368,7 +369,7 @@ export default createWidget('post-menu', { return this.sendWidgetAction('toggleLike'); } - const $heart = $(`[data-post-id=${attrs.id}] .d-icon-heart`); + const $heart = $(`[data-post-id=${attrs.id}] .toggle-like .d-icon`); $heart.closest('button').addClass('has-like'); if (!Ember.testing) { From c29334cf23e32a403fdc0d9289df4c641692fe3e Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 4 Oct 2017 11:41:08 -0400 Subject: [PATCH 078/108] FEATURE: the hide_email_address_taken setting works with the change email address form in user preferences --- config/locales/server.en.yml | 4 +- lib/email_updater.rb | 12 ++++-- spec/components/email_updater_spec.rb | 21 ++++++++++ spec/requests/users_email_controller_spec.rb | 40 +++++++++++++++----- 4 files changed, 61 insertions(+), 16 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 9c49b2588a..faf758f5c9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2660,11 +2660,11 @@ en: title: "Account already exists" subject_template: "[%{email_prefix}] Account already exists" text_body_template: | - You just tried to create an account at %{site_name}. However, an account already exists for %{email}. + You just tried to create an account at %{site_name}, or tried to change the email of an account to %{email}. However, an account already exists for %{email}. If you forgot your password, [reset it now](%{base_url}/password-reset). - If you didn’t try to create an account for %{email}, don’t worry – you can safely ignore this message. + If you didn’t try to create an account for %{email} or change your email address, don’t worry – you can safely ignore this message. If you have any questions, [contact our friendly staff](%{base_url}/about). diff --git a/lib/email_updater.rb b/lib/email_updater.rb index 12a2878194..ddb0627fef 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -27,12 +27,16 @@ class EmailUpdater EmailValidator.new(attributes: :email).validate_each(self, :email, email) if existing_user = User.find_by_email(email) - error_message = 'change_email.error' - error_message << '_staged' if existing_user.staged? - errors.add(:base, I18n.t(error_message)) + if SiteSetting.hide_email_address_taken + Jobs.enqueue(:critical_user_email, type: :account_exists, user_id: existing_user.id) + else + error_message = 'change_email.error' + error_message << '_staged' if existing_user.staged? + errors.add(:base, I18n.t(error_message)) + end end - if errors.blank? + if errors.blank? && existing_user.nil? args = { old_email: @user.email, new_email: email, diff --git a/spec/components/email_updater_spec.rb b/spec/components/email_updater_spec.rb index 034fd163c7..cd2270f467 100644 --- a/spec/components/email_updater_spec.rb +++ b/spec/components/email_updater_spec.rb @@ -131,4 +131,25 @@ describe EmailUpdater do end end end + + context 'hide_email_address_taken is enabled' do + before do + SiteSetting.hide_email_address_taken = true + end + + let(:user) { Fabricate(:user, email: old_email) } + let(:existing) { Fabricate(:user, email: new_email) } + let(:updater) { EmailUpdater.new(user.guardian, user) } + + it "doesn't error if user exists with new email" do + updater.change_to(existing.email) + expect(updater.errors).to be_blank + expect(user.email_change_requests).to be_empty + end + + it 'sends an email to the owner of the account with the new email' do + Jobs.expects(:enqueue).once.with(:critical_user_email, has_entries(type: :account_exists, user_id: existing.id)) + updater.change_to(existing.email) + end + end end diff --git a/spec/requests/users_email_controller_spec.rb b/spec/requests/users_email_controller_spec.rb index 28d5d89e83..78c83cbe3e 100644 --- a/spec/requests/users_email_controller_spec.rb +++ b/spec/requests/users_email_controller_spec.rb @@ -96,20 +96,40 @@ describe UsersEmailController do context 'when the new email address is taken' do let!(:other_user) { Fabricate(:coding_horror) } - it 'raises an error' do - put "/u/#{user.username}/preferences/email.json", params: { - email: other_user.email - } + context 'hide_email_address_taken is disabled' do + before do + SiteSetting.hide_email_address_taken = false + end - expect(response).to_not be_success + it 'raises an error' do + put "/u/#{user.username}/preferences/email.json", params: { + email: other_user.email + } + + expect(response).to_not be_success + end + + it 'raises an error if there is whitespace too' do + put "/u/#{user.username}/preferences/email.json", params: { + email: "#{other_user.email} " + } + + expect(response).to_not be_success + end end - it 'raises an error if there is whitespace too' do - put "/u/#{user.username}/preferences/email.json", params: { - email: "#{other_user.email} " - } + context 'hide_email_address_taken is enabled' do + before do + SiteSetting.hide_email_address_taken = true + end - expect(response).to_not be_success + it 'responds with success' do + put "/u/#{user.username}/preferences/email.json", params: { + email: other_user.email + } + + expect(response).to be_success + end end end From ddbd1d5ab8ae423d333f1ef446e050f58d12753b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 4 Oct 2017 15:08:51 -0400 Subject: [PATCH 079/108] allow regex options on username site settings --- lib/validators/regex_setting_validation.rb | 17 +++++++++++++++ lib/validators/string_setting_validator.rb | 13 +++++------- lib/validators/username_setting_validator.rb | 12 +++++++++-- .../username_setting_validator_spec.rb | 21 +++++++++++++++++++ 4 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 lib/validators/regex_setting_validation.rb diff --git a/lib/validators/regex_setting_validation.rb b/lib/validators/regex_setting_validation.rb new file mode 100644 index 0000000000..cd0df29f49 --- /dev/null +++ b/lib/validators/regex_setting_validation.rb @@ -0,0 +1,17 @@ +module RegexSettingValidation + + def initialize_regex_opts(opts = {}) + @regex = Regexp.new(opts[:regex]) if opts[:regex] + @regex_error = opts[:regex_error] || 'site_settings.errors.regex_mismatch' + end + + def regex_match?(val) + if @regex && !(val =~ @regex) + @regex_fail = true + return false + end + + true + end + +end diff --git a/lib/validators/string_setting_validator.rb b/lib/validators/string_setting_validator.rb index b7ffc2dcf3..e2dda64458 100644 --- a/lib/validators/string_setting_validator.rb +++ b/lib/validators/string_setting_validator.rb @@ -1,8 +1,10 @@ class StringSettingValidator + + include RegexSettingValidation + def initialize(opts = {}) @opts = opts - @regex = Regexp.new(opts[:regex]) if opts[:regex] - @regex_error = opts[:regex_error] || 'site_settings.errors.regex_mismatch' + initialize_regex_opts(opts) end def valid_value?(val) @@ -13,12 +15,7 @@ class StringSettingValidator return false end - if @regex && !(val =~ @regex) - @regex_fail = true - return false - end - - true + regex_match?(val) end def error_message diff --git a/lib/validators/username_setting_validator.rb b/lib/validators/username_setting_validator.rb index d9aa18ad5f..52ae5abd05 100644 --- a/lib/validators/username_setting_validator.rb +++ b/lib/validators/username_setting_validator.rb @@ -1,13 +1,21 @@ class UsernameSettingValidator + + include RegexSettingValidation + def initialize(opts = {}) @opts = opts + initialize_regex_opts(opts) end def valid_value?(val) - !val.present? || User.where(username: val).exists? + !val.present? || (User.where(username: val).exists? && regex_match?(val)) end def error_message - I18n.t('site_settings.errors.invalid_username') + if @regex_fail + I18n.t(@regex_error) + else + I18n.t('site_settings.errors.invalid_username') + end end end diff --git a/spec/components/validators/username_setting_validator_spec.rb b/spec/components/validators/username_setting_validator_spec.rb index 8e06e4fd83..8b302bba35 100644 --- a/spec/components/validators/username_setting_validator_spec.rb +++ b/spec/components/validators/username_setting_validator_spec.rb @@ -17,5 +17,26 @@ describe UsernameSettingValidator do it "returns false if value does not match a user's username" do expect(validator.valid_value?('no way')).to eq(false) end + + context "regex support" do + let!(:darthvader) { Fabricate(:user, username: 'darthvader') } + let!(:luke) { Fabricate(:user, username: 'luke') } + + it "returns false if regex doesn't match" do + v = described_class.new(regex: 'darth') + expect(v.valid_value?('luke')).to eq(false) + expect(v.valid_value?('vader')).to eq(false) + end + + it "returns true if regex matches" do + v = described_class.new(regex: 'darth') + expect(v.valid_value?('darthvader')).to eq(true) + end + + it "returns false if regex matches but username doesn't match a user" do + v = described_class.new(regex: 'darth') + expect(v.valid_value?('darthmaul')).to eq(false) + end + end end end From 051b49efdb29ea9611d3dccfcfed53aa29a454df Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 4 Oct 2017 12:48:14 -0400 Subject: [PATCH 080/108] FIX: Properly encode string literals in hbs compiler --- lib/javascripts/widget-hbs-compiler.js.es6 | 7 ++++++- script/test_hbs_compiler.rb | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index 0f84f1c03d..2561d9362d 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -11,7 +11,12 @@ function sexp(value) { let result = []; value.hash.pairs.forEach(p => { - result.push(`"${p.key}": ${p.value.original}`); + let pValue = p.value.original; + if (p.value.type === "StringLiteral") { + pValue = JSON.stringify(pValue); + } + + result.push(`"${p.key}": ${pValue}`); }); return `{ ${result.join(", ")} }`; diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb index d18a41fc17..773908c5b4 100644 --- a/script/test_hbs_compiler.rb +++ b/script/test_hbs_compiler.rb @@ -3,7 +3,7 @@ template = <<~HBS {{a}} {{{htmlValue}}} {{#if state.category}} - {{attach widget="category-display" attrs=(hash category=state.category)}} + {{attach widget="category-display" attrs=(hash category=state.category someNumber=123 someString="wat")}} {{/if}} HBS From 6d0bf287b5d24b131a86188e1691cf3770fdfc06 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 4 Oct 2017 14:53:09 -0400 Subject: [PATCH 081/108] Allow more extensibility for the post menu buttons --- .../javascripts/discourse/widgets/link.js.es6 | 6 ++-- .../discourse/widgets/post-menu.js.es6 | 32 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/link.js.es6 b/app/assets/javascripts/discourse/widgets/link.js.es6 index 5774ccc763..a762a1e74d 100644 --- a/app/assets/javascripts/discourse/widgets/link.js.es6 +++ b/app/assets/javascripts/discourse/widgets/link.js.es6 @@ -31,8 +31,10 @@ export default createWidget('link', { }, buildAttributes(attrs) { - return { href: this.href(attrs), - title: attrs.title ? I18n.t(attrs.title) : this.label(attrs) }; + return { + href: this.href(attrs), + title: attrs.title ? I18n.t(attrs.title) : this.label(attrs) + }; }, label(attrs) { diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index b36fdb21bb..f6a0665ca4 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -29,6 +29,18 @@ function registerButton(name, builder) { _builders[name] = builder; } +export function buildButton(name, widget) { + let { attrs, state, siteSettings } = widget; + let builder = _builders[name]; + if (builder) { + let button = builder(attrs, state, siteSettings); + if (button && !button.id) { + button.id = name; + } + return button; + } +} + registerButton('like', attrs => { if (!attrs.showLike) { return; } const className = attrs.liked ? 'toggle-like has-like fade-out' : 'toggle-like like'; @@ -181,6 +193,7 @@ registerButton('bookmark', attrs => { } return { + id: attrs.bookmarked ? 'bookmark' : 'unbookmark', action: 'toggleBookmark', title: attrs.bookmarked ? "bookmarks.created" : "bookmarks.not_bookmarked", className, @@ -198,13 +211,13 @@ registerButton('admin', attrs => { registerButton('delete', attrs => { if (attrs.canRecoverTopic) { - return { action: 'recoverPost', title: 'topic.actions.recover', icon: 'undo', className: 'recover' }; + return { id: 'recover_topic', action: 'recoverPost', title: 'topic.actions.recover', icon: 'undo', className: 'recover' }; } else if (attrs.canDeleteTopic) { - return { action: 'deletePost', title: 'topic.actions.delete', icon: 'trash-o', className: 'delete' }; + return { id: 'delete_topic', action: 'deletePost', title: 'topic.actions.delete', icon: 'trash-o', className: 'delete' }; } else if (attrs.canRecover) { - return { action: 'recoverPost', title: 'post.controls.undelete', icon: 'undo', className: 'recover' }; + return { id: 'recover', action: 'recoverPost', title: 'post.controls.undelete', icon: 'undo', className: 'recover' }; } else if (attrs.canDelete) { - return { action: 'deletePost', title: 'post.controls.delete', icon: 'trash-o', className: 'delete' }; + return { action: 'delete', title: 'post.controls.delete', icon: 'trash-o', className: 'delete' }; } }); @@ -229,13 +242,10 @@ export default createWidget('post-menu', { buildKey: attrs => `post-menu-${attrs.id}`, - attachButton(name, attrs) { - const builder = _builders[name]; - if (builder) { - const buttonAtts = builder(attrs, this.state, this.siteSettings); - if (buttonAtts) { - return this.attach(this.settings.buttonType, buttonAtts); - } + attachButton(name) { + let buttonAtts = buildButton(name, this); + if (buttonAtts) { + return this.attach(this.settings.buttonType, buttonAtts); } }, From 4aa30cba2ea9668239cf5981592ba899b84f6409 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 4 Oct 2017 15:52:40 -0400 Subject: [PATCH 082/108] Extensibility for Post manage menu --- .../discourse/widgets/post-admin-menu.js.es6 | 113 +++++++++++------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 index dabbdc421b..1532e5a70c 100644 --- a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 @@ -10,52 +10,85 @@ createWidget('post-admin-menu-button', jQuery.extend(ButtonClass, { } })); +export function buildManageButtons(widget) { + let { attrs, currentUser } = widget; + + if (!currentUser) { + return []; + } + + let contents = []; + if (!attrs.isWhisper && currentUser.staff) { + const buttonAtts = { + action: 'togglePostType', + icon: 'shield', + className: 'toggle-post-type' + }; + + if (attrs.isModeratorAction) { + buttonAtts.label = 'post.controls.revert_to_regular'; + } else { + buttonAtts.label = 'post.controls.convert_to_moderator'; + } + contents.push(buttonAtts); + } + + if (attrs.canManage) { + contents.push({ + icon: 'cog', + label: 'post.controls.rebake', + action: 'rebakePost', + className: 'rebuild-html' + }); + + if (attrs.hidden) { + contents.push({ + icon: 'eye', + label: 'post.controls.unhide', + action: 'unhidePost', + className: 'unhide-post' + }); + } + } + + if (currentUser.admin) { + contents.push({ + icon: 'user', + label: 'post.controls.change_owner', + action: 'changePostOwner', + className: 'change-owner' + }); + } + + if (attrs.wiki) { + contents.push({ + action: 'toggleWiki', + label: 'post.controls.unwiki', + icon: 'pencil-square-o', + className: 'wiki wikied' + }); + } else { + contents.push({ + action: 'toggleWiki', + label: 'post.controls.wiki', + icon: 'pencil-square-o', + className: 'wiki' + }); + } + + return contents; +} + export default createWidget('post-admin-menu', { tagName: 'div.post-admin-menu.popup-menu', - html(attrs) { + html() { const contents = []; contents.push(h('h3', I18n.t('admin_title'))); - if (!attrs.isWhisper && this.currentUser.staff) { - const buttonAtts = { action: 'togglePostType', icon: 'shield', className: 'toggle-post-type' }; - - if (attrs.isModeratorAction) { - buttonAtts.label = 'post.controls.revert_to_regular'; - } else { - buttonAtts.label = 'post.controls.convert_to_moderator'; - } - contents.push(this.attach('post-admin-menu-button', buttonAtts)); - } - - if (attrs.canManage) { - contents.push(this.attach('post-admin-menu-button', { - icon: 'cog', label: 'post.controls.rebake', action: 'rebakePost', className: 'rebuild-html' - })); - - if (attrs.hidden) { - contents.push(this.attach('post-admin-menu-button', { - icon: 'eye', label: 'post.controls.unhide', action: 'unhidePost', className: 'unhide-post' - })); - } - } - - if (this.currentUser.admin) { - contents.push(this.attach('post-admin-menu-button', { - icon: 'user', label: 'post.controls.change_owner', action: 'changePostOwner', className: 'change-owner' - })); - } - - // toggle Wiki button - if (attrs.wiki) { - contents.push(this.attach('post-admin-menu-button', { - action: 'toggleWiki', label: 'post.controls.unwiki', icon: 'pencil-square-o', className: 'wiki wikied' - })); - } else { - contents.push(this.attach('post-admin-menu-button', { - action: 'toggleWiki', label: 'post.controls.wiki', icon: 'pencil-square-o', className: 'wiki' - })); - } + buildManageButtons(this).forEach(b => { + contents.push(this.attach('post-admin-menu-button', b)); + }); return contents; }, From e2124355459efbadc8cae374a5c3bb2a48111e07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 4 Oct 2017 22:08:41 +0200 Subject: [PATCH 083/108] FIX: redirect to top wasn't working --- app/jobs/regular/update_top_redirection.rb | 9 ++++++--- app/models/user_option.rb | 6 +++--- spec/models/user_option_spec.rb | 16 ++++++++++------ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/jobs/regular/update_top_redirection.rb b/app/jobs/regular/update_top_redirection.rb index 776cdfca7d..592dc6b840 100644 --- a/app/jobs/regular/update_top_redirection.rb +++ b/app/jobs/regular/update_top_redirection.rb @@ -3,9 +3,12 @@ module Jobs class UpdateTopRedirection < Jobs::Base def execute(args) - if user = User.find_by(id: args[:user_id]) - user.user_option.update_column(:last_redirected_to_top_at, args[:redirected_at]) - end + return if args[:user_id].blank? || args[:redirected_at].blank? + + UserOption + .where(user_id: args[:user_id]) + .limit(1) + .update_all(last_redirected_to_top_at: args[:redirected_at]) end end diff --git a/app/models/user_option.rb b/app/models/user_option.rb index cf8d135997..6ce2f12ef5 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -77,7 +77,7 @@ class UserOption < ActiveRecord::Base $redis.expire(key, delay) # delay the update - Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.id, redirected_at: Time.zone.now) + Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.user_id, redirected_at: Time.zone.now) end def should_be_redirected_to_top @@ -92,10 +92,10 @@ class UserOption < ActiveRecord::Base return if user.trust_level > 0 && user.last_seen_at && user.last_seen_at > 1.month.ago # top must be in the top_menu - return unless SiteSetting.top_menu =~ /(^|\|)top(\||$)/i + return unless SiteSetting.top_menu[/\btop\b/i] # not enough topics - return unless period = SiteSetting.min_redirected_to_top_period(1.days.ago) + return unless period = SiteSetting.min_redirected_to_top_period(1.day.ago) if !user.seen_before? || (user.trust_level == 0 && !redirected_to_top_yet?) update_last_redirected_to_top! diff --git a/spec/models/user_option_spec.rb b/spec/models/user_option_spec.rb index d494896cef..832de55276 100644 --- a/spec/models/user_option_spec.rb +++ b/spec/models/user_option_spec.rb @@ -56,20 +56,20 @@ describe UserOption do let!(:user) { Fabricate(:user) } it "should have no reason when `SiteSetting.redirect_users_to_top_page` is disabled" do - SiteSetting.expects(:redirect_users_to_top_page).returns(false) + SiteSetting.redirect_users_to_top_page = false expect(user.user_option.redirected_to_top).to eq(nil) end context "when `SiteSetting.redirect_users_to_top_page` is enabled" do - before { SiteSetting.expects(:redirect_users_to_top_page).returns(true) } + before { SiteSetting.redirect_users_to_top_page = true } it "should have no reason when top is not in the `SiteSetting.top_menu`" do - SiteSetting.expects(:top_menu).returns("latest") + SiteSetting.top_menu = "latest" expect(user.user_option.redirected_to_top).to eq(nil) end context "and when top is in the `SiteSetting.top_menu`" do - before { SiteSetting.expects(:top_menu).returns("latest|top") } + before { SiteSetting.top_menu = "latest|top" } it "should have no reason when there are not enough topics" do SiteSetting.expects(:min_redirected_to_top_period).returns(nil) @@ -87,8 +87,12 @@ describe UserOption do end it "should have a reason for the first visit" do - expect(user.user_option.redirected_to_top).to eq(reason: I18n.t('redirected_to_top_reasons.new_user'), - period: :monthly) + freeze_time do + delay = SiteSetting.active_user_rate_limit_secs / 2 + Jobs.expects(:enqueue_in).with(delay, :update_top_redirection, user_id: user.id, redirected_at: Time.zone.now) + + expect(user.user_option.redirected_to_top).to eq(reason: I18n.t('redirected_to_top_reasons.new_user'), period: :monthly) + end end it "should not have a reason for next visits" do From cf4e6e2f5be95022ddc5d09d67187e646a54034f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 4 Oct 2017 16:12:00 -0400 Subject: [PATCH 084/108] FIX: `deletePost` action was incorrect called `delete` --- app/assets/javascripts/discourse/widgets/post-menu.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index f6a0665ca4..7dda8da45b 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -217,7 +217,7 @@ registerButton('delete', attrs => { } else if (attrs.canRecover) { return { id: 'recover', action: 'recoverPost', title: 'post.controls.undelete', icon: 'undo', className: 'recover' }; } else if (attrs.canDelete) { - return { action: 'delete', title: 'post.controls.delete', icon: 'trash-o', className: 'delete' }; + return { id: 'delete', action: 'deletePost', title: 'post.controls.delete', icon: 'trash-o', className: 'delete' }; } }); From 4771b0a99f5f147a7348f5dc4b4170543a9b9133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 4 Oct 2017 23:04:24 +0200 Subject: [PATCH 085/108] FIX: user fields in invite signups were broken --- .../javascripts/discourse/controllers/invites-show.js.es6 | 2 +- app/controllers/invites_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/invites-show.js.es6 b/app/assets/javascripts/discourse/controllers/invites-show.js.es6 index 15acf96ddd..2553a01a13 100644 --- a/app/assets/javascripts/discourse/controllers/invites-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invites-show.js.es6 @@ -61,7 +61,7 @@ export default Ember.Controller.extend(PasswordValidation, UsernameValidation, N username: this.get('accountUsername'), name: this.get('accountName'), password: this.get('accountPassword'), - userCustomFields + user_custom_fields: userCustomFields } }).then(result => { if (result.success) { diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 152e23392f..2866ff2dcb 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -31,7 +31,7 @@ class InvitesController < ApplicationController def perform_accept_invitation params.require(:id) - params.permit(:username, :name, :password, :user_custom_fields) + params.permit(:username, :name, :password, user_custom_fields: {}) invite = Invite.find_by(invite_key: params[:id]) if invite.present? From f5a2ed99b0c714902da79f7374100cf997289685 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 4 Oct 2017 17:04:29 -0400 Subject: [PATCH 086/108] FIX: deleting category background images sometimes has no effect --- app/models/category.rb | 4 ---- lib/stylesheet/manager.rb | 2 +- spec/components/stylesheet/manager_spec.rb | 26 ++++++++++++++++++++++ spec/models/category_spec.rb | 8 ------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/app/models/category.rb b/app/models/category.rb index 1b9032b8d4..72fe1b6e49 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -108,10 +108,6 @@ class Category < ActiveRecord::Base Category.reset_topic_ids_cache end - def self.last_updated_at - order('updated_at desc').limit(1).pluck(:updated_at).first.to_i - end - def self.scoped_to_permissions(guardian, permission_types) if guardian.try(:is_admin?) all diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb index 85cd6bd083..92442b5606 100644 --- a/lib/stylesheet/manager.rb +++ b/lib/stylesheet/manager.rb @@ -257,7 +257,7 @@ class Stylesheet::Manager def color_scheme_digest cs = theme&.color_scheme - category_updated = Category.where("uploaded_background_id IS NOT NULL").last_updated_at + category_updated = Category.where("uploaded_background_id IS NOT NULL").pluck(:updated_at).map(&:to_i).sum if cs || category_updated > 0 Digest::SHA1.hexdigest "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{Stylesheet::Manager.last_file_updated}-#{category_updated}" diff --git a/spec/components/stylesheet/manager_spec.rb b/spec/components/stylesheet/manager_spec.rb index 98a28e4b9e..07571887fa 100644 --- a/spec/components/stylesheet/manager_spec.rb +++ b/spec/components/stylesheet/manager_spec.rb @@ -64,4 +64,30 @@ describe Stylesheet::Manager do # our theme better have a name with the theme_id as part of it expect(new_link).to include("/stylesheets/desktop_theme_#{theme.id}_") end + + describe 'color_scheme_digest' do + it "changes with category background image" do + theme = Theme.new( + name: 'parent', + user_id: -1 + ) + category1 = Fabricate(:category, uploaded_background_id: 123, updated_at: 1.week.ago) + category2 = Fabricate(:category, uploaded_background_id: 456, updated_at: 2.days.ago) + + manager = Stylesheet::Manager.new(:desktop_theme, theme.key) + + digest1 = manager.color_scheme_digest + + category2.update_attributes(uploaded_background_id: 789, updated_at: 1.day.ago) + + digest2 = manager.color_scheme_digest + expect(digest2).to_not eq(digest1) + + category1.update_attributes(uploaded_background_id: nil, updated_at: 5.minutes.ago) + + digest3 = manager.color_scheme_digest + expect(digest3).to_not eq(digest2) + expect(digest3).to_not eq(digest1) + end + end end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index e61de161b1..7c9fbc565b 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -19,14 +19,6 @@ describe Category do expect(cats.errors[:name]).to be_present end - describe "last_updated_at" do - it "returns a number value of when the category was last updated" do - last = Category.last_updated_at - expect(last).to be_present - expect(last.to_i).to eq(last) - end - end - describe "resolve_permissions" do it "can determine read_restricted" do read_restricted, resolved = Category.resolve_permissions(everyone: :full) From b0557c6692b6e88878bbba965d08bf2985514350 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 5 Oct 2017 11:48:42 +0800 Subject: [PATCH 087/108] UX: Allow users to remove a remind me topic timer. --- .../components/edit-topic-timer-form.js.es6 | 50 +++++++++++ .../controllers/edit-topic-timer.js.es6 | 79 +++++++---------- .../javascripts/discourse/routes/topic.js.es6 | 1 + .../components/edit-topic-timer-form.hbs | 36 ++++++++ .../templates/modal/edit-topic-timer.hbs | 85 +++++++------------ .../javascripts/discourse/templates/topic.hbs | 21 +++-- .../base/edit-topic-status-update-modal.scss | 10 +++ app/assets/stylesheets/desktop/topic.scss | 4 +- app/models/topic.rb | 5 ++ app/serializers/topic_view_serializer.rb | 10 +++ config/locales/client.en.yml | 4 +- spec/models/topic_spec.rb | 16 ++++ 12 files changed, 213 insertions(+), 108 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs diff --git a/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 new file mode 100644 index 0000000000..421972b834 --- /dev/null +++ b/app/assets/javascripts/discourse/components/edit-topic-timer-form.js.es6 @@ -0,0 +1,50 @@ +import { default as computed, observes, on } from "ember-addons/ember-computed-decorators"; + +import { + PUBLISH_TO_CATEGORY_STATUS_TYPE, + OPEN_STATUS_TYPE, + DELETE_STATUS_TYPE, + REMINDER_TYPE, + CLOSE_STATUS_TYPE +} from 'discourse/controllers/edit-topic-timer'; + +export default Ember.Component.extend({ + selection: Ember.computed.alias('topicTimer.status_type'), + autoOpen: Ember.computed.equal('selection', OPEN_STATUS_TYPE), + autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE), + autoDelete: Ember.computed.equal('selection', DELETE_STATUS_TYPE), + publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE), + reminder: Ember.computed.equal('selection', REMINDER_TYPE), + showTimeOnly: Ember.computed.or('autoOpen', 'autoDelete', 'reminder'), + + @computed('topicTimer.updateTime', 'loading', 'publishToCategory', 'topicTimer.category_id') + saveDisabled(updateTime, loading, publishToCategory, topicTimerCategoryId) { + return Ember.isEmpty(updateTime) || + loading || + (publishToCategory && !topicTimerCategoryId); + }, + + @computed("topic.visible") + excludeCategoryId(visible) { + if (visible) return this.get('topic.category_id'); + }, + + @on('init') + @observes("topicTimer", "topicTimer.execute_at", "topicTimer.duration") + _setUpdateTime() { + let time = null; + const executeAt = this.get('topicTimer.execute_at'); + + if (executeAt && this.get("topicTimer.based_on_last_post")) { + time = this.get("topicTimer.duration"); + } else if (executeAt) { + const closeTime = moment(executeAt); + + if (closeTime > moment()) { + time = closeTime.format("YYYY-MM-DD HH:mm"); + } + } + + this.set("topicTimer.updateTime", time); + } +}); diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 index fdb66c1a65..e1a122a37a 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 @@ -1,67 +1,51 @@ -import { default as computed, observes } from "ember-addons/ember-computed-decorators"; +import { default as computed } from "ember-addons/ember-computed-decorators"; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import TopicTimer from 'discourse/models/topic-timer'; import { popupAjaxError } from 'discourse/lib/ajax-error'; export const CLOSE_STATUS_TYPE = 'close'; -const OPEN_STATUS_TYPE = 'open'; +export const OPEN_STATUS_TYPE = 'open'; export const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category'; -const DELETE_STATUS_TYPE = 'delete'; -const REMINDER_TYPE = 'reminder'; +export const DELETE_STATUS_TYPE = 'delete'; +export const REMINDER_TYPE = 'reminder'; export default Ember.Controller.extend(ModalFunctionality, { loading: false, - updateTime: null, - topicTimer: Ember.computed.alias("model.topic_timer"), - selection: Ember.computed.alias('model.topic_timer.status_type'), - autoOpen: Ember.computed.equal('selection', OPEN_STATUS_TYPE), - autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE), - autoDelete: Ember.computed.equal('selection', DELETE_STATUS_TYPE), - publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE), - reminder: Ember.computed.equal('selection', REMINDER_TYPE), - - showTimeOnly: Ember.computed.or('autoOpen', 'autoDelete', 'reminder'), + isPublic: "true", @computed("model.closed") - timerTypes(closed) { + publicTimerTypes(closed) { return [ { id: CLOSE_STATUS_TYPE, name: I18n.t(closed ? 'topic.temp_open.title' : 'topic.auto_close.title'), }, { id: OPEN_STATUS_TYPE, name: I18n.t(closed ? 'topic.auto_reopen.title' : 'topic.temp_close.title') }, { id: PUBLISH_TO_CATEGORY_STATUS_TYPE, name: I18n.t('topic.publish_to_category.title') }, - { id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') }, + { id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') } + ]; + }, + + @computed() + privateTimerTypes() { + return [ { id: REMINDER_TYPE, name: I18n.t('topic.reminder.title') } ]; }, - @computed('updateTime', 'loading', 'publishToCategory', 'topicTimer.category_id') - saveDisabled(updateTime, loading, publishToCategory, topicTimerCategoryId) { - return Ember.isEmpty(updateTime) || - loading || - (publishToCategory && !topicTimerCategoryId); - }, - - @computed("model.visible") - excludeCategoryId(visible) { - if (visible) return this.get('model.category_id'); - }, - - @observes("topicTimer.execute_at", "topicTimer.duration") - _setUpdateTime() { - if (!this.get('topicTimer.execute_at')) return; - - let time = null; - - if (this.get("topicTimer.based_on_last_post")) { - time = this.get("topicTimer.duration"); - } else if (this.get("topicTimer.execute_at")) { - const closeTime = moment(this.get('topicTimer.execute_at')); - - if (closeTime > moment()) { - time = closeTime.format("YYYY-MM-DD HH:mm"); - } + @computed("isPublic", 'publicTimerTypes', 'privateTimerTypes') + selections(isPublic, publicTimerTypes, privateTimerTypes) { + if (isPublic === 'true') { + return publicTimerTypes; + } else { + return privateTimerTypes; } + }, - this.set("updateTime", time); + @computed('isPublic', 'model.topic_timer', 'model.private_topic_timer') + topicTimer(isPublic, publicTopicTimer, privateTopicTimer) { + if (isPublic === 'true') { + return publicTopicTimer; + } else { + return privateTopicTimer; + } }, _setTimer(time, statusType) { @@ -85,10 +69,11 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('model.closed', result.closed); } else { + const topicTimer = this.get('isPublic') === 'true' ? 'topic_timer' : 'private_topic_timer'; + this.set(`model.${topicTimer}`, Ember.Object.create({})); + this.setProperties({ - topicTimer: Ember.Object.create({}), selection: null, - updateTime: null }); } }).catch(error => { @@ -98,11 +83,11 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { saveTimer() { - this._setTimer(this.get("updateTime"), this.get('selection')); + this._setTimer(this.get("topicTimer.updateTime"), this.get('topicTimer.status_type')); }, removeTimer() { - this._setTimer(null, this.get('selection')); + this._setTimer(null, this.get('topicTimer.status_type')); } } }); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 4e17be6c6b..5086722534 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -54,6 +54,7 @@ const TopicRoute = Discourse.Route.extend({ showTopicStatusUpdate() { const model = this.modelFor('topic'); model.set('topic_timer', Ember.Object.create(model.get('topic_timer'))); + model.set('private_topic_timer', Ember.Object.create(model.get('private_topic_timer'))); showModal('edit-topic-timer', { model }); this.controllerFor('modal').set('modalClass', 'edit-topic-timer-modal'); }, diff --git a/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs new file mode 100644 index 0000000000..e231080081 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/edit-topic-timer-form.hbs @@ -0,0 +1,36 @@ +
+
+ {{combo-box content=timerTypes value=selection width="50%"}} +
+ +
+ {{#if showTimeOnly}} + {{future-date-input + input=topicTimer.updateTime + statusType=selection + includeWeekend=true + basedOnLastPost=false}} + {{else if publishToCategory}} +
+ + {{category-select-box + value=topicTimer.category_id + excludeCategoryId=excludeCategoryId}} +
+ + {{future-date-input + input=topicTimer.updateTime + statusType=selection + includeWeekend=true + categoryId=topicTimer.category_id + basedOnLastPost=false}} + {{else if autoClose}} + {{future-date-input + input=topicTimer.updateTime + statusType=selection + includeWeekend=true + basedOnLastPost=topicTimer.based_on_last_post + lastPostedAt=model.last_posted_at}} + {{/if}} +
+
diff --git a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs index dfe6826bdb..4de56698a3 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-topic-timer.hbs @@ -1,54 +1,35 @@ -
- {{#d-modal-body title="topic.topic_status_update.title" autoFocus="false"}} -
- {{combo-box content=timerTypes value=selection width="50%"}} -
+{{#d-modal-body title="topic.topic_status_update.title" autoFocus="false"}} +
+ -
- {{#if showTimeOnly}} - {{future-date-input - input=updateTime - statusType=selection - includeWeekend=true - basedOnLastPost=false}} - {{else if publishToCategory}} -
- - {{category-select-box - value=topicTimer.category_id - excludeCategoryId=excludeCategoryId}} -
- - {{future-date-input - input=updateTime - statusType=selection - includeWeekend=true - categoryId=topicTimer.category_id - basedOnLastPost=false}} - {{else if autoClose}} - {{future-date-input - input=updateTime - statusType=selection - includeWeekend=true - basedOnLastPost=topicTimer.based_on_last_post - lastPostedAt=model.last_posted_at}} - {{/if}} -
- {{/d-modal-body}} - - - + + {{edit-topic-timer-form + topic=model + topicTimer=topicTimer + timerTypes=selections + updateTime=updateTime + closeModal="closeModal"}} +{{/d-modal-body}} + + diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 4f0fcddacd..8c9eed6e50 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -199,12 +199,21 @@
{{/if}} - {{topic-timer-info - statusType=model.topic_timer.status_type - executeAt=model.topic_timer.execute_at - basedOnLastPost=model.topic_timer.based_on_last_post - duration=model.topic_timer.duration - categoryId=model.topic_timer.category_id}} + {{#if model.private_topic_timer.execute_at}} + {{topic-timer-info + statusType=model.private_topic_timer.status_type + executeAt=model.private_topic_timer.execute_at + duration=model.private_topic_timer.duration}} + {{/if}} + + {{#if model.topic_timer.execute_at}} + {{topic-timer-info + statusType=model.topic_timer.status_type + executeAt=model.topic_timer.execute_at + basedOnLastPost=model.topic_timer.based_on_last_post + duration=model.topic_timer.duration + categoryId=model.topic_timer.category_id}} + {{/if}} {{#if session.showSignupCta}} {{! replace "Log In to Reply" with the infobox }} diff --git a/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss index 1c17981d7b..3a9424ca47 100644 --- a/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss +++ b/app/assets/stylesheets/common/base/edit-topic-status-update-modal.scss @@ -8,8 +8,18 @@ text-align: left; } + .radios { + margin-bottom: 10px; + } + label { + vertical-align: middle; display: inline-block; + padding-right: 5px; + + input { + vertical-align: middle; + } } .btn.pull-right { diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index 8448400be7..98f7ec65fd 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -69,8 +69,8 @@ } .topic-status-info { - border-top: 1px solid $primary-low; - padding-top: 10px; + border-top: 1px solid $primary-low; + padding: 10px 0px; height: 20px; max-width: 757px; } diff --git a/app/models/topic.rb b/app/models/topic.rb index 161d5ba361..59822cd256 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -411,6 +411,7 @@ class Topic < ActiveRecord::Base def reload(options = nil) @post_numbers = nil @public_topic_timer = nil + @private_topic_timer = nil super(options) end @@ -1002,6 +1003,10 @@ SQL @public_topic_timer ||= topic_timers.find_by(deleted_at: nil, public_type: true) end + def private_topic_timer(user) + @private_topic_Timer ||= topic_timers.find_by(deleted_at: nil, public_type: false, user_id: user.id) + end + def delete_topic_timer(status_type, by_user: Discourse.system_user) options = { status_type: status_type } options.merge!(user: by_user) unless TopicTimer.public_types[status_type] diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index c5ccc91789..df70f3592b 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -61,6 +61,7 @@ class TopicViewSerializer < ApplicationSerializer :message_archived, :tags, :topic_timer, + :private_topic_timer, :unicode_title, :message_bus_last_id, :participant_count @@ -242,6 +243,15 @@ class TopicViewSerializer < ApplicationSerializer TopicTimerSerializer.new(object.topic.public_topic_timer, root: false) end + def include_private_topic_timer? + scope.user + end + + def private_topic_timer + timer = object.topic.private_topic_timer(scope.user) + TopicTimerSerializer.new(timer, root: false) + end + def tags object.topic.tags.map(&:name) end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ecc0e9f5e4..28f3c24433 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1555,12 +1555,14 @@ en: deleted: "The topic has been deleted" topic_status_update: - title: "Set Topic Timer" + title: "Topic Timer" save: "Set Timer" num_of_hours: "Number of hours:" remove: "Remove Timer" publish_to: "Publish To:" when: "When:" + public_timer_types: Topic Timers + private_timer_types: User Topic Timers auto_update_input: none: "" later_today: "Later today" diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index ce3ece7a4f..706b2ee497 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1167,6 +1167,22 @@ describe Topic do end end + describe '#private_topic_timer' do + let(:user) { Fabricate(:user) } + + let(:topic_timer) do + Fabricate(:topic_timer, + public_type: false, + user: user, + status_type: TopicTimer.private_types[:reminder] + ) + end + + it 'should return the right record' do + expect(topic_timer.topic.private_topic_timer(user)).to eq(topic_timer) + end + end + describe '#set_or_create_timer' do let(:topic) { Fabricate.build(:topic) } From a25851032a4d72df8836f3477748b62f9a0f2c12 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 5 Oct 2017 13:59:21 +0800 Subject: [PATCH 088/108] Update Sidekiq. --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8d33704952..ecbdbd6565 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -287,7 +287,7 @@ GEM ffi (>= 1.0.6) msgpack (>= 0.4.3) trollop (>= 1.16.2) - redis (3.3.3) + redis (3.3.5) redis-namespace (1.5.3) redis (~> 3.0, >= 3.0.4) rinku (2.0.2) @@ -352,11 +352,11 @@ GEM shoulda-context (1.2.2) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (5.0.4) + sidekiq (5.0.5) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) - redis (~> 3.3, >= 3.3.3) + redis (>= 3.3.4, < 5) simple-rss (1.3.1) slop (3.6.0) sprockets (3.7.1) From 5dc4b469be6524ba26448f8f4a56ab5a63dcd53c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 5 Oct 2017 14:53:03 +0800 Subject: [PATCH 089/108] Remove unused code for Redis Sentinel. --- app/models/global_setting.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index e141e151e1..9c5e67cccf 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -118,13 +118,6 @@ class GlobalSetting c[:db] = redis_db if redis_db != 0 c[:db] = 1 if Rails.env == "test" - if redis_sentinels.present? - c[:sentinels] = redis_sentinels.split(",").map do |address| - host, port = address.split(":") - { host: host, port: port } - end.to_a - end - c.freeze end end From b2127600fb3213950bbdeedbcd9b99117623107f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 5 Oct 2017 15:57:08 +0800 Subject: [PATCH 090/108] Remove use of concurrent timer for Redis failover. * Uses the same logic for Postgres failover. --- lib/discourse_redis.rb | 39 ++++++++++++------------- spec/components/discourse_redis_spec.rb | 5 ++-- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index bcfc120c6b..c2b49c3232 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -2,6 +2,7 @@ # A wrapper around redis that namespaces keys with the current site id # require_dependency 'cache' + class DiscourseRedis class FallbackHandler include Singleton @@ -14,16 +15,24 @@ class DiscourseRedis @running = false @mutex = Mutex.new @slave_config = DiscourseRedis.slave_config - @timer_task = init_timer_task @message_bus_keepalive_interval = MessageBus.keepalive_interval end def verify_master - synchronize do - return if @timer_task.running? - end + synchronize { return if @thread && @thread.alive? } - @timer_task.execute + @thread = Thread.new do + loop do + begin + thread = Thread.new { initiate_fallback_to_master } + thread.join + break if synchronize { @master } + sleep 10 + ensure + thread.kill + end + end + end end def initiate_fallback_to_master @@ -31,10 +40,10 @@ class DiscourseRedis begin slave_client = ::Redis::Client.new(@slave_config) - logger.info "#{log_prefix}: Checking connection to master server..." + logger.warn "#{log_prefix}: Checking connection to master server..." if slave_client.call([:info]).split("\r\n").include?(MASTER_LINK_STATUS) - logger.info "#{log_prefix}: Master server is active, killing all connections to slave..." + logger.warn "#{log_prefix}: Master server is active, killing all connections to slave..." self.master = true @@ -67,18 +76,8 @@ class DiscourseRedis end end - def running? - @timer_task.running? - end - private - def init_timer_task - Concurrent::TimerTask.new(execution_interval: 10) do |task| - task.shutdown if initiate_fallback_to_master - end - end - def synchronize @mutex.synchronize { yield } end @@ -101,7 +100,7 @@ class DiscourseRedis def resolve(client = nil) if !@fallback_handler.master - @fallback_handler.verify_master unless @fallback_handler.running? + @fallback_handler.verify_master return @slave_options end @@ -114,7 +113,7 @@ class DiscourseRedis rescue Redis::ConnectionError, Redis::CannotConnectError, RuntimeError => ex raise ex if ex.class == RuntimeError && ex.message != "Name or service not known" @fallback_handler.master = false - @fallback_handler.verify_master unless @fallback_handler.running? + @fallback_handler.verify_master raise ex ensure client.disconnect @@ -182,7 +181,7 @@ class DiscourseRedis :msetnx, :persist, :pexpire, :pexpireat, :psetex, :pttl, :rename, :renamenx, :rpop, :rpoplpush, :rpush, :rpushx, :sadd, :scard, :sdiff, :set, :setbit, :setex, :setnx, :setrange, :sinter, :sismember, :smembers, :sort, :spop, :srandmember, :srem, :strlen, :sunion, :ttl, :type, :watch, :zadd, :zcard, :zcount, :zincrby, :zrange, :zrangebyscore, :zrank, :zrem, :zremrangebyrank, - :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore].each do |m| + :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore, :evalsha, :eval].each do |m| define_method m do |*args| args[0] = "#{namespace}:#{args[0]}" if @namespace DiscourseRedis.ignore_readonly { @redis.send(m, *args) } diff --git a/spec/components/discourse_redis_spec.rb b/spec/components/discourse_redis_spec.rb index 07742fce54..9067ceedbe 100644 --- a/spec/components/discourse_redis_spec.rb +++ b/spec/components/discourse_redis_spec.rb @@ -135,14 +135,13 @@ describe DiscourseRedis do error = RuntimeError.new('Name or service not known') expect { connector.resolve(BrokenRedis.new(error)) }.to raise_error(error) - fallback_handler.instance_variable_get(:@timer_task).shutdown - expect(fallback_handler.running?).to eq(false) + expect(fallback_handler.master).to eq(false) config = connector.resolve expect(config[:host]).to eq(slave_host) expect(config[:port]).to eq(slave_port) - expect(fallback_handler.running?).to eq(true) + expect(fallback_handler.master).to eq(false) ensure fallback_handler.master = true end From beca02c046b9d73558dbc4eb5274ddfdb563af2d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 5 Oct 2017 14:12:07 -0400 Subject: [PATCH 091/108] FIX: moderators couldn't see flagged topics list --- config/routes.rb | 2 +- .../admin/flagged_topics_controller_spec.rb | 32 +++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 2ebd987a42..b3df2161ea 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -193,7 +193,7 @@ Discourse::Application.routes.draw do post "flags/disagree/:id" => "flags#disagree" post "flags/defer/:id" => "flags#defer" - resources :flagged_topics, constraints: AdminConstraint.new + resources :flagged_topics, constraints: StaffConstraint.new resources :themes, constraints: AdminConstraint.new post "themes/import" => "themes#import" diff --git a/spec/requests/admin/flagged_topics_controller_spec.rb b/spec/requests/admin/flagged_topics_controller_spec.rb index 594caff1ed..8f768294bc 100644 --- a/spec/requests/admin/flagged_topics_controller_spec.rb +++ b/spec/requests/admin/flagged_topics_controller_spec.rb @@ -1,19 +1,33 @@ require 'rails_helper' RSpec.describe Admin::FlaggedTopicsController do - let(:admin) { Fabricate(:admin) } let!(:flag) { Fabricate(:flag) } - before do - sign_in(admin) + shared_examples "successfully retrieve list of flagged topics" do + it "returns a list of flagged topics" do + get "/admin/flagged_topics.json" + expect(response).to be_success + + data = ::JSON.parse(response.body) + expect(data['flagged_topics']).to be_present + expect(data['users']).to be_present + end end - it "returns a list of flagged topics" do - get "/admin/flagged_topics.json" - expect(response).to be_success + context "as admin" do + before do + sign_in(Fabricate(:admin)) + end - data = ::JSON.parse(response.body) - expect(data['flagged_topics']).to be_present - expect(data['users']).to be_present + include_examples "successfully retrieve list of flagged topics" end + + context "as moderator" do + before do + sign_in(Fabricate(:moderator)) + end + + include_examples "successfully retrieve list of flagged topics" + end + end From 7df73c94a069925cd06f0f1376dfb80ef2d352f0 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 5 Oct 2017 14:34:27 -0400 Subject: [PATCH 092/108] Add a hook to decorate extra buttons --- app/assets/javascripts/discourse/widgets/post-menu.js.es6 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index 7dda8da45b..2569ff0489 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -1,4 +1,4 @@ -import { createWidget } from 'discourse/widgets/widget'; +import { applyDecorators, createWidget } from 'discourse/widgets/widget'; import { avatarAtts } from 'discourse/widgets/actions-summary'; import { h } from 'virtual-dom'; @@ -340,7 +340,8 @@ export default createWidget('post-menu', { postControls.push(repliesButton); } - postControls.push(h('div.actions', visibleButtons)); + let extraControls = applyDecorators(this, 'extra-controls', attrs, state); + postControls.push(h('div.actions', visibleButtons.concat(extraControls))); if (state.adminVisible) { postControls.push(this.attach('post-admin-menu', attrs)); } From 07d04aba1d170ebdca262374a956688a14988200 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 5 Oct 2017 14:34:47 -0400 Subject: [PATCH 093/108] Support `{{unless}}` in virtual dom templates --- lib/javascripts/widget-hbs-compiler.js.es6 | 6 +++++- script/test_hbs_compiler.rb | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index 2561d9362d..8b3a20be96 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -140,9 +140,13 @@ class Compiler { } break; case "BlockStatement": + let negate = ''; + switch(node.path.original) { + case 'unless': + negate = '!'; case 'if': - instructions.push(`if (${node.params[0].original}) {`); + instructions.push(`if (${negate}${node.params[0].original}) {`); node.program.body.forEach(child => { instructions = instructions.concat(this.processNode(parentAcc, child)); }); diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb index 773908c5b4..9bfe595261 100644 --- a/script/test_hbs_compiler.rb +++ b/script/test_hbs_compiler.rb @@ -5,6 +5,9 @@ template = <<~HBS {{#if state.category}} {{attach widget="category-display" attrs=(hash category=state.category someNumber=123 someString="wat")}} {{/if}} + {{#unless state.hello}} + XYZ + {{/unless}} HBS ctx = MiniRacer::Context.new(timeout: 15000) From 0c84352386bdebfa77820531d963adca8785a2d7 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 5 Oct 2017 16:13:00 -0400 Subject: [PATCH 094/108] Add support for transformations --- .../javascripts/discourse/widgets/widget.js.es6 | 6 ++++++ lib/javascripts/widget-hbs-compiler.js.es6 | 16 +++++++++------- script/test_hbs_compiler.rb | 8 +++++++- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 339549c3b4..b57049c695 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -91,6 +91,8 @@ function drawWidget(builder, attrs, state) { } } + this.transformed = this.transform(); + let contents = this.html(attrs, state); if (this.name) { const beforeContents = applyDecorators(this, 'before', attrs, state) || []; @@ -173,6 +175,10 @@ export default class Widget { } } + transform() { + return {}; + } + defaultState() { return {}; } diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index 8b3a20be96..7e756f3746 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -1,5 +1,5 @@ function resolve(path) { - if (path.indexOf('settings') === 0) { + if (path.indexOf('settings') === 0 || path.indexOf('transformed') === 0) { return `this.${path}`; } return path; @@ -29,6 +29,8 @@ function argValue(arg) { return sexp(arg.value); } else if (value.type === "PathExpression") { return value.original; + } else if (value.type === "StringLiteral") { + return JSON.stringify(value.value); } } @@ -37,13 +39,13 @@ function mustacheValue(node, state) { switch(path) { case 'attach': - let widgetName = node.hash.pairs.find(p => p.key === "widget").value.value; + let widgetName = argValue(node.hash.pairs.find(p => p.key === "widget")); let attrs = node.hash.pairs.find(p => p.key === "attrs"); if (attrs) { - return `this.attach("${widgetName}", ${argValue(attrs)})`; + return `this.attach(${widgetName}, ${argValue(attrs)})`; } - return `this.attach("${widgetName}", attrs)`; + return `this.attach(${widgetName}, attrs)`; break; case 'yield': @@ -54,7 +56,7 @@ function mustacheValue(node, state) { if (node.params[0].type === "StringLiteral") { value = `"${node.params[0].value}"`; } else if (node.params[0].type === "PathExpression") { - value = node.params[0].original; + value = resolve(node.params[0].original); } if (value) { @@ -146,7 +148,7 @@ class Compiler { case 'unless': negate = '!'; case 'if': - instructions.push(`if (${negate}${node.params[0].original}) {`); + instructions.push(`if (${negate}${resolve(node.params[0].original)}) {`); node.program.body.forEach(child => { instructions = instructions.concat(this.processNode(parentAcc, child)); }); @@ -160,7 +162,7 @@ class Compiler { instructions.push(`}`); break; case 'each': - const collection = node.params[0].original; + const collection = resolve(node.params[0].original); instructions.push(`if (${collection} && ${collection}.length) {`); instructions.push(` ${collection}.forEach(${node.program.blockParams[0]} => {`); node.program.body.forEach(child => { diff --git a/script/test_hbs_compiler.rb b/script/test_hbs_compiler.rb index 9bfe595261..0045b94254 100644 --- a/script/test_hbs_compiler.rb +++ b/script/test_hbs_compiler.rb @@ -5,7 +5,13 @@ template = <<~HBS {{#if state.category}} {{attach widget="category-display" attrs=(hash category=state.category someNumber=123 someString="wat")}} {{/if}} - {{#unless state.hello}} + {{#each transformed.something as |s|}} + {{s.wat}} + {{/each}} + + {{attach widget=settings.widgetName}} + + {{#unless settings.hello}} XYZ {{/unless}} HBS From 2ae06e6fd081f84465108967d911fae9a1dec398 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 5 Oct 2017 17:00:23 -0400 Subject: [PATCH 095/108] More customization for menu items --- .../discourse/widgets/post-admin-menu.js.es6 | 36 +++++++++---------- .../discourse/widgets/post-menu.js.es6 | 7 +++- .../discourse/widgets/widget.js.es6 | 2 +- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 index 1532e5a70c..fd91273949 100644 --- a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 @@ -10,9 +10,7 @@ createWidget('post-admin-menu-button', jQuery.extend(ButtonClass, { } })); -export function buildManageButtons(widget) { - let { attrs, currentUser } = widget; - +export function buildManageButtons(attrs, currentUser) { if (!currentUser) { return []; } @@ -60,20 +58,22 @@ export function buildManageButtons(widget) { }); } - if (attrs.wiki) { - contents.push({ - action: 'toggleWiki', - label: 'post.controls.unwiki', - icon: 'pencil-square-o', - className: 'wiki wikied' - }); - } else { - contents.push({ - action: 'toggleWiki', - label: 'post.controls.wiki', - icon: 'pencil-square-o', - className: 'wiki' - }); + if (attrs.canWiki) { + if (attrs.wiki) { + contents.push({ + action: 'toggleWiki', + label: 'post.controls.unwiki', + icon: 'pencil-square-o', + className: 'wiki wikied' + }); + } else { + contents.push({ + action: 'toggleWiki', + label: 'post.controls.wiki', + icon: 'pencil-square-o', + className: 'wiki' + }); + } } return contents; @@ -86,7 +86,7 @@ export default createWidget('post-admin-menu', { const contents = []; contents.push(h('h3', I18n.t('admin_title'))); - buildManageButtons(this).forEach(b => { + buildManageButtons(this.attrs, this.currentUser).forEach(b => { contents.push(this.attach('post-admin-menu-button', b)); }); diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index 2569ff0489..80ab77838e 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -249,6 +249,11 @@ export default createWidget('post-menu', { } }, + menuItems() { + let result = this.siteSettings.post_menu.split('|'); + return result; + }, + html(attrs, state) { const { siteSettings } = this; @@ -260,7 +265,7 @@ export default createWidget('post-menu', { const allButtons = []; let visibleButtons = []; - const orderedButtons = siteSettings.post_menu.split('|'); + const orderedButtons = this.menuItems(); // If the post is a wiki, make Edit more prominent if (attrs.wiki) { diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index b57049c695..d81b2b5395 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -91,7 +91,7 @@ function drawWidget(builder, attrs, state) { } } - this.transformed = this.transform(); + this.transformed = this.transform(this.attrs, this.state); let contents = this.html(attrs, state); if (this.name) { From 5822c64bdc835b46f20b1781771788a1d0a15a7a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 5 Oct 2017 17:47:52 -0400 Subject: [PATCH 096/108] FIX: If you can manage a post you can wiki it --- app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 index fd91273949..0a29b82bb2 100644 --- a/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-admin-menu.js.es6 @@ -58,7 +58,7 @@ export function buildManageButtons(attrs, currentUser) { }); } - if (attrs.canWiki) { + if (attrs.canManage || attrs.canWiki) { if (attrs.wiki) { contents.push({ action: 'toggleWiki', From e61edfd13ade7643cfab7e780fc7163ab4f19356 Mon Sep 17 00:00:00 2001 From: Jay Pfaffman Date: Thu, 5 Oct 2017 15:20:14 -0700 Subject: [PATCH 097/108] UX: discourse restore -- sort by date --- script/discourse | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/discourse b/script/discourse index c5d16d36e3..85b7fa491a 100755 --- a/script/discourse +++ b/script/discourse @@ -96,7 +96,7 @@ class DiscourseCLI < Thor if !filename puts "You must provide a filename to restore. Did you mean one of the following?\n\n" - Dir["public/backups/default/*"].each do |f| + Dir["public/backups/default/*"].sort_by { |filename| File.mtime(filename) }.reverse.each do |f| puts "#{discourse} restore #{File.basename(f)}" end From 8b71a0e49ff4922b335a34354f3af49573d8642f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 6 Oct 2017 08:27:03 +0800 Subject: [PATCH 098/108] UX: Restore missing border at the end of topic list. --- .../javascripts/discourse/templates/topic.hbs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index 8c9eed6e50..b35d45d2d6 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -206,14 +206,12 @@ duration=model.private_topic_timer.duration}} {{/if}} - {{#if model.topic_timer.execute_at}} - {{topic-timer-info - statusType=model.topic_timer.status_type - executeAt=model.topic_timer.execute_at - basedOnLastPost=model.topic_timer.based_on_last_post - duration=model.topic_timer.duration - categoryId=model.topic_timer.category_id}} - {{/if}} + {{topic-timer-info + statusType=model.topic_timer.status_type + executeAt=model.topic_timer.execute_at + basedOnLastPost=model.topic_timer.based_on_last_post + duration=model.topic_timer.duration + categoryId=model.topic_timer.category_id}} {{#if session.showSignupCta}} {{! replace "Log In to Reply" with the infobox }} From a88f9104693ba4baa9b09b6e1fd2cbc49a6fe185 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 6 Oct 2017 08:33:46 +0800 Subject: [PATCH 099/108] Bump message_bus to 2.0.8. --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ecbdbd6565..64a3964df8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -158,7 +158,7 @@ GEM mail (2.6.6) mime-types (>= 1.16, < 4) memory_profiler (0.9.8) - message_bus (2.0.7) + message_bus (2.0.8) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) From 3efde2618d7c96476285dfe5e2413f270c0ac105 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 6 Oct 2017 10:35:40 +0800 Subject: [PATCH 100/108] UX: Do not display non-human users on group page. https://meta.discourse.org/t/members-of-groups-staff/71437 --- app/controllers/groups_controller.rb | 8 +++++--- spec/requests/groups_controller_spec.rb | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 7691f99cab..2785e69312 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -125,15 +125,17 @@ class GroupsController < ApplicationController order = "#{params[:order]} #{dir} NULLS LAST" end - total = group.users.count - members = group.users + users = group.users.human_users + + total = users.count + members = users .order('NOT group_users.owner') .order(order) .order(username_lower: dir) .limit(limit) .offset(offset) - owners = group.users + owners = users .order(order) .order(username_lower: dir) .where('group_users.owner') diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index f808ed7578..5e4f729a24 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -183,7 +183,9 @@ describe GroupsController do ) end - let(:group) { Fabricate(:group, users: [user1, user2, user3]) } + let(:bot) { Fabricate(:user, id: -999) } + + let(:group) { Fabricate(:group, users: [user1, user2, user3, bot]) } it "should allow members to be sorted by" do get "/groups/#{group.name}/members.json", params: { From d67f0b39ae652ec8b5e6fc1648586795ccf31a27 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 6 Oct 2017 11:13:01 +0800 Subject: [PATCH 101/108] Update annotations. --- app/models/category.rb | 2 +- app/models/post_custom_field.rb | 2 +- app/models/topic.rb | 1 + app/models/upload.rb | 1 + app/models/user.rb | 2 ++ app/models/user_email.rb | 1 + 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/models/category.rb b/app/models/category.rb index 72fe1b6e49..54f59af722 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -560,5 +560,5 @@ end # # index_categories_on_email_in (email_in) UNIQUE # index_categories_on_topic_count (topic_count) -# unique_index_categories_on_name (name) UNIQUE +# unique_index_categories_on_name ((COALESCE(parent_category_id, '-1'::integer)), name) UNIQUE # diff --git a/app/models/post_custom_field.rb b/app/models/post_custom_field.rb index 713d0affbc..999500de52 100644 --- a/app/models/post_custom_field.rb +++ b/app/models/post_custom_field.rb @@ -15,6 +15,6 @@ end # # Indexes # -# index_post_custom_fields_on_name_and_value (name) +# index_post_custom_fields_on_name_and_value (name, "left"(value, 200)) # index_post_custom_fields_on_post_id_and_name (post_id,name) # diff --git a/app/models/topic.rb b/app/models/topic.rb index 59822cd256..f4093cb8f5 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1336,6 +1336,7 @@ end # index_topics_on_bumped_at (bumped_at) # index_topics_on_created_at_and_visible (created_at,visible) # index_topics_on_id_and_deleted_at (id,deleted_at) +# index_topics_on_lower_title (lower((title)::text)) # index_topics_on_pinned_at (pinned_at) # index_topics_on_pinned_globally (pinned_globally) # diff --git a/app/models/upload.rb b/app/models/upload.rb index 329e59714a..148deea227 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -170,6 +170,7 @@ end # # Indexes # +# index_uploads_on_extension (lower((extension)::text)) # index_uploads_on_id_and_url (id,url) # index_uploads_on_sha1 (sha1) UNIQUE # index_uploads_on_url (url) diff --git a/app/models/user.rb b/app/models/user.rb index 3c3d4abc9d..2429418648 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1124,6 +1124,7 @@ end # name :string # seen_notification_id :integer default(0), not null # last_posted_at :datetime +# email :string(513) # password_hash :string(64) # salt :string(32) # active :boolean default(FALSE), not null @@ -1157,6 +1158,7 @@ end # # idx_users_admin (id) # idx_users_moderator (id) +# index_users_on_email (lower((email)::text)) UNIQUE # index_users_on_last_posted_at (last_posted_at) # index_users_on_last_seen_at (last_seen_at) # index_users_on_uploaded_avatar_id (uploaded_avatar_id) diff --git a/app/models/user_email.rb b/app/models/user_email.rb index 485c3f0bc0..c7b35eb3ce 100644 --- a/app/models/user_email.rb +++ b/app/models/user_email.rb @@ -42,6 +42,7 @@ end # # Indexes # +# index_user_emails_on_email (lower((email)::text)) UNIQUE # index_user_emails_on_user_id (user_id) # index_user_emails_on_user_id_and_primary (user_id,primary) UNIQUE # From 4ba5e678d8136400eb0f800a2b601865c4a585b0 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 6 Oct 2017 11:39:00 +0800 Subject: [PATCH 102/108] Require dependencies to enable live reload in dev for Sidekiq. --- app/jobs/onceoff/fix_primary_emails_for_staged_users.rb | 2 ++ app/jobs/regular/crawl_topic_link.rb | 1 + app/jobs/regular/notify_mailing_list_subscribers.rb | 2 ++ app/jobs/scheduled/dashboard_stats.rb | 4 ++++ lib/discourse_hub.rb | 1 + plugins/discourse-narrative-bot/jobs/narrative_init.rb | 3 +++ 6 files changed, 13 insertions(+) diff --git a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb index 15fe43a14b..36a55c64d5 100644 --- a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb +++ b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb @@ -1,3 +1,5 @@ +require_dependency 'user_destroyer' + module Jobs class FixPrimaryEmailsForStagedUsers < Jobs::Onceoff def execute_onceoff(args) diff --git a/app/jobs/regular/crawl_topic_link.rb b/app/jobs/regular/crawl_topic_link.rb index b5077aff85..15c0c01ad0 100644 --- a/app/jobs/regular/crawl_topic_link.rb +++ b/app/jobs/regular/crawl_topic_link.rb @@ -2,6 +2,7 @@ require 'open-uri' require 'nokogiri' require 'excon' require_dependency 'retrieve_title' +require_dependency 'topic_link' module Jobs class CrawlTopicLink < Jobs::Base diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index 6b8647a36b..015cb65fdc 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -1,3 +1,5 @@ +require_dependency 'post' + module Jobs class NotifyMailingListSubscribers < Jobs::Base diff --git a/app/jobs/scheduled/dashboard_stats.rb b/app/jobs/scheduled/dashboard_stats.rb index 75977e7acb..9556bd7ea1 100644 --- a/app/jobs/scheduled/dashboard_stats.rb +++ b/app/jobs/scheduled/dashboard_stats.rb @@ -1,3 +1,7 @@ +require_dependency 'admin_dashboard_data' +require_dependency 'group' +require_dependency 'group_message' + module Jobs class DashboardStats < Jobs::Scheduled every 30.minutes diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb index 940800be17..3ed120aaa9 100644 --- a/lib/discourse_hub.rb +++ b/lib/discourse_hub.rb @@ -1,4 +1,5 @@ require_dependency 'version' +require_dependency 'site_setting' module DiscourseHub diff --git a/plugins/discourse-narrative-bot/jobs/narrative_init.rb b/plugins/discourse-narrative-bot/jobs/narrative_init.rb index e2e9219bee..68fb003f11 100644 --- a/plugins/discourse-narrative-bot/jobs/narrative_init.rb +++ b/plugins/discourse-narrative-bot/jobs/narrative_init.rb @@ -1,3 +1,6 @@ +require_dependency 'i18n' +require_dependency 'user' + module Jobs class NarrativeInit < Jobs::Base sidekiq_options queue: 'critical' From 4552840e54e8b66a0feaa525a7e7837bfed4ebcb Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 6 Oct 2017 14:33:38 +0800 Subject: [PATCH 103/108] REFACTOR: DRY update code that uses duplicated logic. --- lib/post_creator.rb | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 42b7055823..f4bf70f560 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -92,21 +92,27 @@ class PostCreator return false end - # Make sure max_allowed_message_recipients setting is respected if @opts[:target_usernames].present? && !skip_validations? && !@user.staff? - errors[:base] << I18n.t(:max_pm_recepients, recipients_limit: SiteSetting.max_allowed_message_recipients) if @opts[:target_usernames].split(',').length > SiteSetting.max_allowed_message_recipients - return false if errors[:base].present? - end + names = @opts[:target_usernames].split(',') - # Make sure none of the users have muted the creator - names = @opts[:target_usernames] - if names.present? && !skip_validations? && !@user.staff? - users = User.where(username: names.split(',').flatten).pluck(:id, :username).to_h + # Make sure max_allowed_message_recipients setting is respected + max_allowed_message_recipients = SiteSetting.max_allowed_message_recipients + + if names.length > max_allowed_message_recipients + errors[:base] << I18n.t(:max_pm_recepients, + recipients_limit: max_allowed_message_recipients + ) + + return false + end + + # Make sure none of the users have muted the creator + users = User.where(username: names).pluck(:id, :username).to_h MutedUser.where(user_id: users.keys, muted_user_id: @user.id).pluck(:user_id).each do |m| errors[:base] << I18n.t(:not_accepting_pms, username: users[m]) + return false end - return false if errors[:base].present? end if new_topic? From 1477a0e91031c9a142e96e748ff7e80896dbbda4 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 6 Oct 2017 14:28:26 +0200 Subject: [PATCH 104/108] Adds a rake task for refreshing posts received via email This is useful when the email_reply_trimmer gem was updated and you want to apply those changes to existing posts. --- lib/email/receiver.rb | 24 +++++++++++++++--------- lib/tasks/posts.rake | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 69569f82b5..0cbefdd160 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -620,6 +620,12 @@ module Email def create_post_with_attachments(options = {}) # deal with attachments + options[:raw] = add_attachments(options[:raw], options[:user].id, options) + + create_post(options) + end + + def add_attachments(raw, user_id, options = {}) attachments.each do |attachment| tmp = Tempfile.new(["discourse-email-attachment", File.extname(attachment.filename)]) begin @@ -627,19 +633,19 @@ module Email File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } # create the upload for the user opts = { for_group_message: options[:is_group_message] } - upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(options[:user].id) + upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(user_id) if upload && upload.errors.empty? # try to inline images - if attachment.content_type.start_with?("image/") - if options[:raw][attachment.url] - options[:raw].sub!(attachment.url, upload.url) - elsif options[:raw][/\[image:.*?\d+[^\]]*\]/i] - options[:raw].sub!(/\[image:.*?\d+[^\]]*\]/i, attachment_markdown(upload)) + if attachment.content_type&.start_with?("image/") + if raw[attachment.url] + raw.sub!(attachment.url, upload.url) + elsif raw[/\[image:.*?\d+[^\]]*\]/i] + raw.sub!(/\[image:.*?\d+[^\]]*\]/i, attachment_markdown(upload)) else - options[:raw] << "\n\n#{attachment_markdown(upload)}\n\n" + raw << "\n\n#{attachment_markdown(upload)}\n\n" end else - options[:raw] << "\n\n#{attachment_markdown(upload)}\n\n" + raw << "\n\n#{attachment_markdown(upload)}\n\n" end end ensure @@ -647,7 +653,7 @@ module Email end end - create_post(options) + raw end def attachment_markdown(upload) diff --git a/lib/tasks/posts.rake b/lib/tasks/posts.rake index 8a4bc69b6a..6c13d53ae4 100644 --- a/lib/tasks/posts.rake +++ b/lib/tasks/posts.rake @@ -238,3 +238,27 @@ task 'posts:defer_all_flags' => :environment do puts "", "#{flags_deferred} flags deferred!", "" end + +desc 'Refreshes each post that was received via email' +task 'posts:refresh_emails', [:topic_id] => [:environment] do |_, args| + posts = Post.where.not(raw_email: nil).where(via_email: true) + posts = posts.where(topic_id: args[:topic_id]) if args[:topic_id] + + updated = 0 + total = posts.count + + posts.find_each do |post| + receiver = Email::Receiver.new(post.raw_email) + + body, elided = receiver.select_body + body = receiver.add_attachments(body || '', post.user_id) + body << Email::Receiver.elided_html(elided) if elided.present? + + post.revise(Discourse.system_user, { raw: body }, skip_revision: true, skip_validations: true) + updated += 1 + + print_status(updated, total) + end + + puts "", "Done. #{updated} posts updated.", "" +end From 3bdd8f57c1a8b95fc361c55d1be995f43040551d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 6 Oct 2017 16:37:28 +0200 Subject: [PATCH 105/108] FIX: invited staged users would sometimes not get notified of replies --- app/models/topic_user.rb | 23 ++++++++--------------- app/services/post_alerter.rb | 2 +- lib/email/receiver.rb | 13 +++++++------ lib/post_creator.rb | 7 ++----- spec/components/email/receiver_spec.rb | 10 +++++++--- 5 files changed, 25 insertions(+), 30 deletions(-) diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 3404fc90d4..a66b6c41af 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -38,26 +38,19 @@ class TopicUser < ActiveRecord::Base end def auto_notification(user_id, topic_id, reason, notification_level) - if TopicUser.where("user_id = :user_id AND topic_id = :topic_id AND (notifications_reason_id IS NULL OR - (notification_level < :notification_level AND notification_level > :normal_notification_level))", - user_id: user_id, topic_id: topic_id, notification_level: notification_level, - normal_notification_level: notification_levels[:regular]).exists? - change(user_id, topic_id, - notification_level: notification_level, - notifications_reason_id: reason - ) - end + should_change = TopicUser + .where(user_id: user_id, topic_id: topic_id) + .where("notifications_reason_id IS NULL OR (notification_level < :min AND notification_level > :max)", min: notification_level, max: notification_levels[:regular]) + .exists? + + change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) if should_change end - def auto_notification_for_staging(user_id, topic_id, reason) - topic_user = TopicUser.find_or_initialize_by(user_id: user_id, topic_id: topic_id) - topic_user.notification_level = notification_levels[:watching] - topic_user.notifications_reason_id = reason - topic_user.save + def auto_notification_for_staging(user_id, topic_id, reason, notification_level=notification_levels[:watching]) + change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) end def unwatch_categories!(user, category_ids) - track_threshold = user.user_option.auto_track_topics_after_msecs sql = < SiteSetting.maximum_staged_users_per_email - topic.add_moderator_post(sender, I18n.t("emails.incoming.maximum_staged_user_per_email_reached")) + post.topic.add_moderator_post(sender, I18n.t("emails.incoming.maximum_staged_user_per_email_reached")) return end end diff --git a/lib/post_creator.rb b/lib/post_creator.rb index f4bf70f560..dc28b66bce 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -508,12 +508,9 @@ class PostCreator if @user.staged TopicUser.auto_notification_for_staging(@user.id, @topic.id, TopicUser.notification_reasons[:auto_watch]) - elsif @user.user_option.notification_level_when_replying === NotificationLevels.topic_levels[:watching] - TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:watching]) - elsif @user.user_option.notification_level_when_replying === NotificationLevels.topic_levels[:regular] - TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:regular]) else - TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], NotificationLevels.topic_levels[:tracking]) + notification_level = @user.user_option.notification_level_when_replying || NotificationLevels.topic_levels[:tracking] + TopicUser.auto_notification(@user.id, @topic.id, TopicUser.notification_reasons[:created_post], notification_level) end end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 839f36192f..c6eee47b72 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -419,9 +419,13 @@ describe Email::Receiver do it "invites everyone in the chain but emails configured as 'incoming' (via reply, group or category)" do expect { process(:cc) }.to change(Topic, :count) - emails = Topic.last.allowed_users.joins(:user_emails).pluck(:"user_emails.email") - expect(emails.size).to eq(3) - expect(emails).to include("someone@else.com", "discourse@bar.com", "wat@bar.com") + + topic = Topic.last + + emails = topic.allowed_users.joins(:user_emails).pluck(:"user_emails.email") + expect(emails).to contain_exactly("someone@else.com", "discourse@bar.com", "wat@bar.com") + + expect(topic.topic_users.count).to eq(3) end it "cap the number of staged users created per email" do From 3a29ba71a37e658a4eaf533dfc091c55e5c94e4c Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 6 Oct 2017 10:28:19 -0400 Subject: [PATCH 106/108] Update translations --- config/locales/client.ar.yml | 511 ++++++++++-------- config/locales/client.el.yml | 3 + config/locales/client.fi.yml | 8 +- config/locales/client.fr.yml | 24 +- config/locales/client.it.yml | 4 + config/locales/client.ja.yml | 48 +- config/locales/client.ko.yml | 16 +- config/locales/client.nb_NO.yml | 5 + config/locales/client.pl_PL.yml | 24 +- config/locales/client.uk.yml | 12 + config/locales/server.el.yml | 15 +- config/locales/server.fi.yml | 109 +++- config/locales/server.it.yml | 26 +- config/locales/server.pl_PL.yml | 4 + .../config/locales/client.tr_TR.yml | 2 +- .../config/locales/client.uk.yml | 7 +- .../config/locales/server.tr_TR.yml | 2 +- .../config/locales/server.uk.yml | 23 +- 18 files changed, 588 insertions(+), 255 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index c8ce6b7cd5..46e883ccb2 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -2029,25 +2029,25 @@ ar: like: "أعجبهم هذا" vote: "صوت لهذا" by_you: - off_topic: "لقد ابلغت ان المنشور خارج الموضوع" - spam: "لقد ابلغت ان المنشور سبام" - inappropriate: "لقد ابلغت ان المنشور غير لائق" - notify_moderators: "لقد ابلغت المشرفين" + off_topic: "لقد ابلغت ان هذا المنشور خارج الموضوع" + spam: "لقد ابلغت ان هذا المنشور سبام" + inappropriate: "لقد ابلغت ان هذا المنشور غير لائق" + notify_moderators: "لقد ابلغت المشرفين عن هذا المنشور" notify_user: "لقد أرسلت رسالة إلى هذا العضو" - bookmark: "لقد علّمت هذا المنشور" + bookmark: "لقد وضعت علامة مرجعية علي هذا المنشور" like: "أعجبت بهذا المنشور" vote: "لقد صوّت على هذا المنشور" by_you_and_others: off_topic: zero: "أنت أبلّغت بأن هذا المنشور خارج الموضوع." - one: "أنت وآخر أبلّغتما بأن هذا المنشور خارج الموضوع." + one: "أنت وشخص اخر أبلّغتما بأن هذا المنشور خارج الموضوع." two: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" few: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" many: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" other: "أنت و {{count}} آخرون أبلغتم بأن هذا المنشور خارج الموضوع" spam: zero: "أنت أبلّغت بأن هذا هذا المنشور سبام." - one: "أنت وآخر أبلّغتما بأن هذا المنشور سبام." + one: "أنت وشخص آخر أبلّغتما بأن هذا المنشور سبام." two: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور سبام." few: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور سبام." many: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور سبام." @@ -2060,12 +2060,12 @@ ar: many: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور غير لائق." other: "أنت و {{count}} آخرون أبلّغتم بأن هذا المنشور غير لائق." notify_moderators: - zero: "أنت أبلّغت المشرفين." - one: "أنت و شخص آخر أبلّغتما المشرفين." - two: "أنت و {{count}} آخرون أبلّغتم المشرفين." - few: "أنت و {{count}} آخرون أبلّغتم المشرفين." - many: "أنت و {{count}} آخرون أبلّغتم المشرفين." - other: "أنت و {{count}} آخرون أبلّغتم المشرفين." + zero: "أنت أبلّغت المشرفين عن هذا المنشور." + one: "أنت و شخص آخر أبلّغتما المشرفين عن هذا المنشور." + two: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." + few: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." + many: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." + other: "أنت و {{count}} آخرون أبلّغتم المشرفين عن هذا المنشور." notify_user: zero: "أنت أرسلت رسالة لهذا العضو." one: "أنت و شخص اخر ارسلتما رسالة لهذا العضو." @@ -2074,91 +2074,99 @@ ar: many: "أنت و {{count}} آخرون ارسلتم رسالة لهذا العضو." other: "أنت و {{count}} آخرون ارسلتم رسالة لهذا العضو." bookmark: - zero: "أنت عَلَّمتَ هذه المشاركة." - one: "أنت و شخص آخر عَلَّمتُما هذه المشاركة." - two: "أنت و {{count}} آخران عَلَّمتُم هذه المشاركة." - few: "أنت و {{count}} آخرون عَلَّمتُم هذه المشاركة." - many: "أنت و {{count}} آخرون عَلَّمتُم هذه المشاركة." - other: "أنت و {{count}} آخرون عَلَّمتُم هذه المشاركة." + zero: "أنت وضعت علامة مرجعية علي هذا المنشور" + one: "أنت و شخص آخر وضعتما علامة مرجعية علي هذا المنشور" + two: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" + few: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" + many: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" + other: "أنت و {{count}} آخرون وضعتم علامة مرجعية علي هذا المنشور" like: - zero: "أعجبك هذا" - one: "أعجب هذا شخصا واحدا غيرك" - two: "أعجب هذا شخصين غيرك" - few: "أعجب هذا {{count}} أشخاص غيرك" - many: "أعجب هذا {{count}} شخصا غيرك" - other: "أعجب هذا {{count}} شخص غيرك" + zero: "أنت اعجبت بهذا المنشور" + one: "أنت و شخص آخر اعجبتما بهذا المنشور" + two: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" + few: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" + many: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" + other: "أنت و {{count}} آخرون اعجبتم بهذا المنشور" vote: - zero: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - one: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - two: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - few: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - many: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" - other: "أنت و {{count}} أشخاص أخرين صوتو لهذا الموضوع" + zero: "أنت صوت علي هذا المنشور" + one: "أنت و شخص آخر صوتما علي هذا المنشور" + two: "أنت و {{count}} آخرون صوتم علي هذا المنشور" + few: "أنت و {{count}} آخرون صوتم علي هذا المنشور" + many: "أنت و {{count}} آخرون صوتم علي هذا المنشور" + other: "أنت و {{count}} آخرون صوتم علي هذا المنشور" by_others: off_topic: - zero: "لم يتم الاشارة لهذا كخارج عن الموضوع." - one: "شخص أشار لهذا كخارج عن الموضوع." - two: "شخصان أشارا لهذا كخارج عن الموضوع." - few: "{{count}} أشخاص أشاروا لهذا كخارج عن الموضوع." - many: "{{count}} شخص أشار لهذا كخارج عن الموضوع." - other: "{{count}} شخص أشار لهذا كخارج عن الموضوع." + zero: "لم يبلغ احد بأن هذا المنشور خارج الموضوع" + one: "عضو واحد أبلغ بأن هذا المنشور خارج الموضوع" + two: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" + few: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" + many: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" + other: "{{count}} عضو أبلغ بأن هذا المنشور خارج الموضوع" spam: - zero: "لم يتم الاشارة لهذا كغير مفيد," - one: "شخص أشار لهذا كغير مفيد." - two: "شخصان أشارا لهذا كغير مفيد." - few: "{{count}} أشخاص أشاروا لهذا كغير مفيد." - many: "{{count}} شخص أشار لهذا كغير مفيد." - other: "{{count}} شخص أشار لهذا كغير مفيد." + zero: "لم يبلّغ احد بأن هذا المنشور سبام." + one: "عضو واحد أبلّغ بأن هذا المنشور سبام." + two: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." + few: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." + many: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." + other: "{{count}} عضو أبلّغ بأن هذا المنشور سبام." inappropriate: - zero: "لم تتم الإشارة لهذا كغير ملائم." - one: "شخص أشار لهذا كغير ملائم." - two: "شخصان أشارا لهذا كغير ملائم." - few: "{{count}} أشخاص أشاروا لهذا كغير ملائم." - many: "{{count}} شخص أشاروا لهذا كغير ملائم." - other: "{{count}} شخص أشاروا لهذا كغير ملائم." + zero: "لم يبلغ احد بأن هذا المنشور غير لائق." + one: "عضو واحد أبلّغ بأن هذا المنشور غير لائق." + two: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." + few: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." + many: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." + other: "{{count}} عضو أبلّغ بأن هذا المنشور غير لائق." notify_moderators: - zero: "1 عضو علّم هذا للمراقبين" - one: "1 عضو علّم هذا للمراقبين" - two: "{{count}} أعضاء علّمو هذا للمراقبين" - few: "{{count}} أعضاء علّمو هذا للمراقبين" - many: "{{count}} أعضاء علّمو هذا للمراقبين" - other: "{{count}} أعضاء علّمو هذا للمراقبين" + zero: "لم يبلغ احد المشرفين عن هذا المنشور" + one: "عضو واحد أبلّغ المشرفين عن هذا المنشور" + two: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" + few: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" + many: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" + other: "{{count}} عضو أبلّغ المشرفين عن هذا المنشور" notify_user: - zero: "لم يُرسل شيء لهذا المستخدم" - one: "أرسل شخص واحد رسالة لهذا المستخدم" - two: "أرسل شخصين رسالة لهذا المستخدم" - few: "أرسل {{count}} أشخاص رسالة لهذا المستخدم" - many: "أرسل {{count}} شخصا رسالة لهذا المستخدم" - other: "أرسل {{count}} شخص رسالة لهذا المستخدم" + zero: "لم يرسل احد رسالة لهذا العضو" + one: "عضو واحد ارسل رسالة لهذا العضو" + two: "{{count}} عضو ارسل رسالة لهذا العضو" + few: "{{count}} عضو ارسل رسالة لهذا العضو" + many: "{{count}} عضو ارسل رسالة لهذا العضو" + other: "{{count}} عضو ارسل رسالة لهذا العضو" bookmark: - zero: "لم يعلّم أحد هذه المشاركة" - one: "شخص واحد علّم هذه المشاركة" - two: "شخصان علّما هذه المشاركة" - few: "{{count}} أشخاص علّموا هذه المشاركة" - many: "{{count}} شخصًا علّموا هذه المشاركة" - other: "{{count}} شخص علّموا هذه المشاركة" + zero: "لم يضع احد علامة مرجعية علي هذا المنشور" + one: "عضو واحد وضع علامة مرجعية علي هذا المنشور" + two: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" + few: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" + many: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" + other: "{{count}} عضو وضع علامة مرجعية علي هذا المنشور" like: - zero: "لم يعجب هذا أحدا" - one: "أعجب هذا شخص واحد" - two: "أعجب هذا شخصان" - few: "أعجب هذا {{count}} أشخاص" - many: "أعجب هذا {{count}} شخصا" - other: "أعجب هذا {{count}} شخص" + zero: "لم يعجب احد بهذا المنشور" + one: "عضو واحد اعجب بهذا المنشور" + two: "{{count}} عضو اعجب بهذا المنشور" + few: "{{count}} عضو اعجب بهذا المنشور" + many: "{{count}} عضو اعجب بهذا المنشور" + other: "{{count}} عضو اعجب بهذا المنشور" vote: - zero: "لم يصوّت أحد على هذه المشاركة" - one: "صوّت شخص واحد على هذه المشاركة" - two: "صوّت شخصان على هذه المشاركة" - few: "صوّت {{count}} أشخاص على هذه المشاركة" - many: "صوّت {{count}} شخصا على هذه المشاركة" - other: "صوّت {{count}} شخص على هذه المشاركة" + zero: "لم يصوت احد علي هذا المنشور" + one: "عضو واحد صوت علي هذا المنشور" + two: "{{count}} عضو صوت علي هذا المنشور" + few: "{{count}} عضو صوت علي هذا المنشور" + many: "{{count}} عضو صوت علي هذا المنشور" + other: "{{count}} عضو صوت علي هذا المنشور" delete: confirm: zero: "لا شيء لحذفه." - one: "أمتأكد من حذف المشاركة؟" - two: "أمتأكد من حذف المشاركتين؟" - few: "أمتأكد من حذف المشاركات هذه؟" - many: "أمتأكد من حذف المشاركات هذه؟" - other: "أمتأكد من حذف المشاركات هذه؟" + one: "هل انت متاكد انك تريد حذف هذا المنشور؟" + two: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + few: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + many: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + other: "هل انت متاكد انك تريد حذف كل هذة المنشورات؟" + merge: + confirm: + zero: "لا يوجد منشورات لدمجها" + one: "هل انت متاكد انك تريد دمج هذا المنشور؟" + two: "هل انت متاكد انك تريد دمج هذان المنشوران؟" + few: "هل انت متاكد انك تريد دمج الـ {{count}} منشور؟" + many: "هل انت متاكد انك تريد دمج الـ {{count}} منشور؟" + other: "هل انت متاكد انك تريد دمج الـ {{count}} منشور؟" revisions: controls: first: "أول مراجعة" @@ -2168,19 +2176,32 @@ ar: hide: "أخفِ المراجعة" show: "أظهر المراجعة" revert: "ارجع إلى هذه المراجعة" - edit_wiki: "حرّر الويكي" + edit_wiki: "عدل الwiki" edit_post: "عدل المنشور" comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: - button: 'HTML' + title: "اعرض النسخة المنسقة في عمود واحد مع تمييز الاسطر المضافة و المحذوفة" + button: 'عمود واحد' side_by_side: - title: "أظهر فروقات الخرج المصيّر جنبًا إلى جنب" - button: 'HTML' + title: "اعرض الفروقات في النسخة المنسقة جنبا إلي جنب" + button: 'عمودين' side_by_side_markdown: - title: "أظهر فروقات المصدر الخامّ جنبًا إلى جنب" + title: "اعرض الفروقات في النسخة الخام جنبا إلي جنب" + button: 'عمودين خام' + raw_email: + displays: + raw: + title: "اعرض نص الرساله الخام" + button: 'خام' + text_part: + title: "اظهر الجزء النصي من رسالة البريد الالكتروني" + button: 'نص' + html_part: + title: "اظهر جزء الـ HTML من رسالة البريد الالكتروني" + button: 'HTML' category: - can: 'يمكنها…' + can: 'قادر علي…' none: '(غير مصنف)' all: 'كل الأقسام' choose: 'اختر قسم …' @@ -2193,7 +2214,7 @@ ar: tags: "الوسوم" tags_allowed_tags: "اسمح فقط لهذة الوسمة بالاستخدام في هذا القسم." tags_allowed_tag_groups: "اسمح فقط للأوسمة من هذة المجموعات بالاستخدام في هذا القسم." - tags_placeholder: "(اختياري) قائمة العلامات الوصفية المسموح بها" + tags_placeholder: "(اختياري) قائمة الاوسمة المسموح بها" tag_groups_placeholder: "(اختياريّ) قائمة مجموعات الوسوم المسموح بها" topic_featured_link_allowed: "اسمح بالروابط المُميزة بهذا القسم." delete: 'احذف القسم' @@ -2201,7 +2222,7 @@ ar: create_long: 'أنشئ قسم جديد' save: 'احفظ القسم' slug: 'عنوان القسم في الURL' - slug_placeholder: '(اختياريّ) كلمات مفصولة بشَرطة للعنوان' + slug_placeholder: '(اختياريّ) كلمات مفصولة-بشرطة للعنوان' creation_error: حدثت مشكلة أثناء إنشاء القسم. save_error: حدث خطأ في حفظ القسم. name: "اسم القسم" @@ -2213,7 +2234,7 @@ ar: background_color: "لون الخلفية" foreground_color: "لون المقدمة" name_placeholder: "كلمة أو كلمتين على الأكثر" - color_placeholder: "أيّ لون وبّ" + color_placeholder: "أيّ لون متوافق مع الانترنت" delete_confirm: "هل تريد فعلاً حذف هذا القسم؟" delete_error: "حدث خطأ في حذف القسم." list: "عرض الأقسام" @@ -2224,7 +2245,7 @@ ar: special_warning: "تحذير: هذا القسم هو قسم اصلي إعدادات الحماية له لا يمكن تعديلها. إذا لم تكن تريد استخدام هذا القسم، قم بحذفة بدلا من تطويعة لأغراض اخري." images: "الصور" email_in: "تعيين بريد إلكتروني خاص:" - email_in_allow_strangers: "قبول بريد إلكتروني من مستخدمين لا يملكون حسابات" + email_in_allow_strangers: "قبول بريد إلكتروني من زوار لا يملكون حسابات" email_in_disabled: "عُطّل إرسال المشاركات عبر البريد الإلكترونيّ من إعدادات الموقع. لتفعيل نشر المشاركات الجديدة عبر البريد،" email_in_disabled_click: 'قم بتفعيل خيار "email in" في الإعدادات' suppress_from_homepage: "امنع هذا القسم من الظهور في الصفحة الرئيسية." @@ -2235,8 +2256,9 @@ ar: subcategory_list_style: "أسلوب عرض قائمة الأقسام الفرعية:" sort_order: "رتب قائمة الموضوعات حسب:" default_view: "قائمة الموضوعات الإفتراضية" + default_top_period: "فترة الاكثر مشاهدة الافتراضية" allow_badges_label: "السماح بالحصول على الأوسمة في هذا القسم" - edit_permissions: "حرّر التّصاريح" + edit_permissions: "عدل التصاريح" add_permission: "أضف تصريحًا" this_year: "هذه السنة" position: "المكان" @@ -2262,53 +2284,72 @@ ar: description: "لن يتم إشعارك بأي موضوعات جديدة في هذه الأقسام ولن يتم عرضها في قائمة الموضوعات المنشورة مؤخراً." sort_options: default: "افترضى" - likes: "اعجاب" - op_likes: "الاعجابات الاساسية للمنشور" + likes: "الاعجابات" + op_likes: "الاعجابات علي المنشور الاساسي" views: "المشاهدات" posts: "المنشورات" activity: "النشاط" - posters: "البوستر" - category: "قسم" - created: "انشئ " - sort_ascending: 'تنازلى' + posters: "الإعلانات" + category: "القسم" + created: "تاريخ الإنشاء" + sort_ascending: 'تصاعدي' sort_descending: 'تنازلي' + subcategory_list_styles: + rows: "صفوف" + rows_with_featured_topics: "صفوف مع الموضوعات المميزة" + boxes: "مربعات" + boxes_with_featured_topics: "مربعات مع الموضوعات المميزة" flagging: - title: 'شكرا لمساعدتك في إبقاء مجتمعنا نظيفاً.' - action: 'التبليغ عن مشاركة' - take_action: "أجراء العمليه " + title: 'شكرا لمساعدتك في إبقاء مجتمعنا متحضرا.' + action: 'ابلغ عن المنشور' + take_action: "اتخذ اجراء" notify_action: 'رسالة' official_warning: 'تحذير رسمي' - delete_spammer: "احذف ناشر السخام" + delete_spammer: "احذف ناشر السبام" delete_confirm_MF: "{posts, plural,\n zero {ليس للمستخدم أيّة مشاركات}\n other {}\n}\n{topics, plural,\n zero {\n {posts, plural,\n zero {أو مواضيع.}\n other {ليس للمستخدم أيّة مواضيع.}\n }\n }\n other {{posts, plural, zero{.} other{}}}\n}\nأنت على وشك\n{posts, plural,\n zero {{topics, plural, zero {} other {حذف}}}\n other {حذف}\n}\n{posts, plural,\n zero {}\n one {مشاركة واحدة}\n two {مشاركتين}\n few {# مشاركات}\n many {# مشاركة}\n other {# مشاركة}\n}\n{posts, plural, zero {} other {{topics, plural, zero {} other {و}}}}\n{topics, plural,\n zero {}\n one {موضوع واحد}\n two {موضوعين}\n few {# مواضيع}\n many {# موضوعًا}\n other {# موضوع}\n}\n{posts, plural,\n zero {{topics, plural, zero {} other {له، و}}}\n other {{topics, plural, zero {له، و} other {للمستخدم، و}}}\n}\nإزالة حسابه، ومنع التّسجيل من عنوان IP هذا {ip_address}، وإضافة عنوان البريد الإلكترونيّ {email} إلى قائمة منع دائم. أمتأكّد حقًّا من أنّ هذا المستخدم ناشر سخام؟" - yes_delete_spammer: "نعم، احذف ناشر السخام" + yes_delete_spammer: "نعم، احذف ناشر السبام" ip_address_missing: "(N/A)" hidden_email_address: "(مخفي)" - submit_tooltip: "إرسال تبليغ" - take_action_tooltip: "الوصول إلى الحد الأعلى للتبليغات دون انتظار تبليغات أكثر من أعضاء الموقع." - cant: "المعذرة، لا يمكنك التبليغ عن هذه المشاركة في هذه اللحظة." - notify_staff: 'أعلِم الطّاقم بسريّة' + submit_tooltip: "إرسال البلاغ" + take_action_tooltip: "الوصول إلى الحد الأعلى للبلاغات دون انتظار بلاغات أكثر من أعضاء الموقع." + cant: "عذرا، لا يمكنك الابلاغ عن هذا المنشور في هذه اللحظة." + notify_staff: 'ابلغ طاقم العمل بسرية' formatted_name: off_topic: "خارج عن الموضوع" inappropriate: "غير لائق" spam: "هذا سبام" - custom_placeholder_notify_user: "كن محدد, استدلالي ودائما حسن الاخلاق" - custom_placeholder_notify_moderators: "ممكن تزودنا بمعلومات أكثر عن سبب عدم ارتياحك حول هذه المشاركة؟ زودنا ببعض الروابط و الأمثلة قدر الإمكان." + custom_placeholder_notify_user: "كن محدد و كن بناء و دائما كن حسن الخلق" + custom_placeholder_notify_moderators: "يمكنك تزودنا بمعلومات أكثر عن سبب عدم ارتياحك إلي هذا المنشور؟ زودنا ببعض الروابط و الأمثلة قدر الإمكان." custom_message: at_least: zero: "لا تُدخل أيّ محرف" - one: "أدخل محرفًا واحدًا على الأقلّ" - two: "أدخل محرفين على الأقلّ" - few: "أدخل {{count}} محارف على الأقلّ" - many: "أدخل {{count}} محرفًا على الأقلّ" - other: "أدخل {{count}} محرف على الأقلّ" + one: "أدخل حرف واحد على الأقل" + two: "أدخل {{count}} احرف على الأقل" + few: "أدخل {{count}} احرف على الأقل" + many: "أدخل {{count}} احرف على الأقل" + other: "أدخل {{count}} احرف على الأقل" + more: + zero: "{{count}} حرف متبقي علي الحد الادني..." + one: "حرف واحد متبقي علي الحد الادني..." + two: "{{count}} حرف متبقي علي الحد الادني..." + few: "{{count}} حرف متبقي علي الحد الادني..." + many: "{{count}} حرف متبقي علي الحد الادني..." + other: "{{count}} حرف متبقي علي الحد الادني..." + left: + zero: "{{count}} حرف متبقي علي الحد الاقصي..." + one: "حرف واحد متبقي علي الحد الاقصي..." + two: "{{count}} حرف متبقي علي الحد الاقصي..." + few: "{{count}} حرف متبقي علي الحد الاقصي..." + many: "{{count}} حرف متبقي علي الحد الاقصي..." + other: "{{count}} حرف متبقي علي الحد الاقصي..." flagging_topic: - title: "شكرا لمساعدتنا في ابقاء مجتمعنا نضيفا" + title: "شكرا لمساعدتنا في ابقاء المجمتع متحضر" action: "التبليغ عن الموضوع" notify_action: "رسالة" topic_map: title: "ملخص الموضوع" - participants_title: "المشاركون المعتادون" - links_title: "روابط شائعة" + participants_title: "الناشرون المترددون" + links_title: "روابط مشهورة" links_shown: "أظهر روابط أخرى..." clicks: zero: "لا نقرات" @@ -2330,16 +2371,16 @@ ar: warning: help: "هذا تحذير رسمي." bookmarked: - help: "لقد علّمت هذا الموضوع" + help: "لقد وضعت علامة مرجعية علي هذا الموضوع" locked: - help: "هذا الموضوع مغلق ولم يعد يستقبل ردودا" + help: "هذا الموضوع مغلق, لذا فهو لم يعد يستقبل ردودا" archived: - help: "هذا الموضوع مؤرشف، لذا فهو مجمّد ولا يمكن تعديله" + help: "هذا الموضوع مؤرشف، لذا فهو مجمد ولا يمكن تعديله" locked_and_archived: - help: "هذا الموضوع مغلق ومؤرشف، لذا لم يعد يستقبل ردودًا ولا يمكن تغييره" + help: "هذا الموضوع مغلق ومؤرشف، لذا فهو لم يعد يستقبل ردودًا ولا يمكن تغييره" unpinned: title: "غير مثبّت" - help: "هذا الموضوع غير مثبّت لك، وسيُعرض بالترتيب العاديّ" + help: "هذا الموضوع غير مثبّت لك، وسيُعرض بالترتيب العادي" pinned_globally: title: "مثبّت للعموم" help: "هذا الموضوع مثبت بشكل عام, سوف يظهر في مقدمة قائمة اخر الموضوعات وفي القسم الخاصة به." @@ -2347,16 +2388,16 @@ ar: title: "مثبّت" help: "هذا الموضوع مثبّت لك، وسيُعرض أعلى قسمة" invisible: - help: "هذا الموضوع غير مصنف لن يظهر في قائمة التصانيف ولايمكن الدخول عليه الابرابط مباشر." - posts: "مشاركات" - posts_long: "هناك {{number}} مشاركات في هذا الموضوع" + help: "هذا الموضوع غير مدرج, لن يظهر في قائمة الموضوعات ولا يمكن الوصول إلية إلا برابط مباشر" + posts: "منشورات" + posts_long: "هناك {{number}} منشور في هذا الموضوع" posts_likes_MF: | {count, plural, zero {ليس في هذا الموضوع أي رد} one {في هذا الموضوع رد واحد} two {في هذا الموضوع ردان} few {في هذا الموضوع # ردود} many {في هذا الموضوع # ردا} other {في هذا الموضوع # رد}} {ratio, select, low {ونسبة الإعجاب إلى المشاركة عالية} med {ونسبة الإعجاب إلى المشاركة عالية جدا} high {ونسبة الإعجاب إلى المشاركة مهولة} other {}} - original_post: "المشاركة الاصلية" + original_post: "المنشور الاصلي" views: "المشاهدات" views_lowercase: zero: "المشاهدات" @@ -2366,6 +2407,13 @@ ar: many: "المشاهدات" other: "المشاهدات" replies: "الردود" + views_long: + zero: "تم مشاهدة هذا الموضوع {{number}} مرة" + one: "تم مشاهدة هذا الموضوع مرة واحدة" + two: "تم مشاهدة هذا الموضوع {{number}} مرة" + few: "تم مشاهدة هذا الموضوع {{number}} مرة" + many: "تم مشاهدة هذا الموضوع {{number}} مرة" + other: "تم مشاهدة هذا الموضوع {{number}} مرة" activity: "النشاط" likes: "اعجابات" likes_lowercase: @@ -2388,10 +2436,11 @@ ar: history: "تاريخ" changed_by: "الكاتب {{author}}" raw_email: + title: "البريد الإلكتروني الوارد" not_available: "غير متوفر!" categories_list: "قائمة الأقسام" filters: - with_topics: "المواضيع %{filter}" + with_topics: "الموضوعات %{filter}" with_category: "الموضوعات%{filter} في %{category}" latest: title: "الأخيرة" @@ -2402,16 +2451,16 @@ ar: few: "الأخيرة ({{count}})" many: "الأخيرة ({{count}})" other: "الأخيرة ({{count}})" - help: "المواضيع التي فيها مشاركات حديثة" + help: "الموضوعات التي بها منشورات حديثة" hot: title: "نَشط" - help: "مختارات من مواضيع ساخنة" + help: "مختارات من انشط الموضوعات" read: title: "المقروءة" - help: "المواضيع التي قرأتها بترتيب آخر قراءة لها" + help: "المواضيع التي قرأتها بترتيب قرائتك لها" search: title: "بحث" - help: "بحث في كل المواضيع" + help: "بحث في كل الموضوعات" categories: title: "الأقسام" title_in: "قسم - {{categoryName}}" @@ -2425,7 +2474,7 @@ ar: few: "غير المقروءة ({{count}})" many: "غير المقروءة ({{count}})" other: "غير المقروءة ({{count}})" - help: "المواضيع التي تتابعها (أو تراقبها) والتي فيها مشاركات غير مقروءة" + help: "الموضوعات التي تتابعها (أو تراقبها) والتي فيها منشورات غير مقروءة" lower_title_with_count: zero: "1 غير مقررء " one: "1 غير مقروء" @@ -2442,7 +2491,7 @@ ar: many: "{{count}} جديد" other: "{{count}} جديد" lower_title: "جديد" - title: "الجديدة" + title: "جديد" title_with_count: zero: "الجديدة ({{count}})" one: "الجديدة ({{count}})" @@ -2452,11 +2501,11 @@ ar: other: "الجديدة ({{count}})" help: "المواضيع المنشأة في الأيّام القليلة الماضية" posted: - title: "مشاركاتي" - help: "مواضيع شاركت بها " + title: "منشوراتي" + help: "مواضيع نشرت بها " bookmarks: - title: "العلامات" - help: "مواضيع قمت بتفضيلها" + title: "العلامات المرجعية" + help: "موضوعات وضعت عليها علامة مرجعية" category: title: "{{categoryName}}" title_with_count: @@ -2466,30 +2515,30 @@ ar: few: "{{categoryName}} ({{count}})" many: "{{categoryName}} ({{count}})" other: "{{categoryName}} ({{count}})" - help: "آخر الموضوعات في القسم {{categoryName}}" + help: "آخر الموضوعات في قسم {{categoryName}}" top: title: "الأكثر مُشاهدة" - help: "أكثر المواضيع نشاطًا في آخر عام، أو شهر أو أسبوع أو يوم" + help: "أكثر المواضيع نشاطًا في آخر عام أو شهر أو أسبوع أو يوم" all: - title: "كلّها" + title: "كل الوقت" yearly: - title: "السّنويّة" + title: "سنة" quarterly: - title: "الرّبعيّة" + title: "ربع سنة" monthly: - title: "الشّهريّة" + title: "شهر" weekly: - title: "الأسبوعيّة" + title: "اسبوع" daily: - title: "اليوميّة" - all_time: "على مرّ الزّمن" + title: "يوم" + all_time: "كل الوقت" this_year: "سنة" this_quarter: "ربع" this_month: "شهر" this_week: "أسبوع" today: "يوم" - other_periods: "مشاهدة الأفضل" - browser_update: 'للأسف، متصفّحك قديم جدًّا ليعمل عليه هذا الموقع. من فضلك رقّه.' + other_periods: "اعرض الاكثر مشاهدة" + browser_update: 'للأسف، متصفّحك قديم جدًّا ليعمل عليه هذا الموقع. من فضلك حدث المتصفح خاصتك.' permission_types: full: "انشاء / رد / مشاهدة" create_post: "رد / مشاهدة" @@ -2499,19 +2548,19 @@ ar: keyboard_shortcuts_help: title: 'اختصارات لوحة المفاتيح' jump_to: - title: 'الانتقال' + title: 'الانتقال إلي' home: 'g، h الرّئيسيّة' latest: 'g، l الأخيرة' - new: 'g، n الجديد' + new: 'g، n الجديدة' unread: 'g، u غير المقروء' categories: 'g، c الأقسام' - top: 'g, t الأعلى' - bookmarks: 'g، b العلامات' - profile: 'g، p اللاحة' + top: 'g, t الاكثر مشاهدة' + bookmarks: 'g، b العلامات المرجعية' + profile: 'g، p الملف الشخصي' messages: 'g، m الرّسائل' navigation: title: 'التنقّل' - jump: '# الانتقال الى المشاركة #' + jump: '# الانتقال الى المنشور#' back: 'u العودة' up_down: 'k/j نقل المحدد ↑ ↓' open: 'o أو Enter فتح الموضوع المحدد' @@ -2522,26 +2571,27 @@ ar: notifications: 'n فتح الإشعارات' hamburger_menu: '= فتح القائمة الرّئيسيّة' user_profile_menu: 'pفتح قائمة المستخدم' - show_incoming_updated_topics: '. عرض المواضيع المحدثة' + show_incoming_updated_topics: '. عرض الموضوعات المحدثة' + search: '/ او ctrl+shift+s بحث' help: '? فتح مساعدة لوحة المفاتيح' - dismiss_new_posts: 'تجاهل جديد / المشاركات x, r' - dismiss_topics: 'x, t تجاهل المواضيع' - log_out: 'shift+z shift+z الخروج' + dismiss_new_posts: 'x, r تجاهل المنشورات الجديدة' + dismiss_topics: 'x, t تجاهل الموضوعات' + log_out: 'shift+z shift+z تسجيل خروج' actions: title: 'إجراءات' - bookmark_topic: 'f تبديل علامة مرجعية الموضوع' - pin_unpin_topic: 'shift+p تثبيت الموضوع أو إلغاء تثبيته' + bookmark_topic: 'f وضع/ازالة علامة مرجعية علي الموضوع' + pin_unpin_topic: 'shift+p تثبيت/إلغاء تثبيت الموضوع' share_topic: 'shift+s مشاركة الموضوع' - share_post: 's مشاركة المشاركة' - reply_as_new_topic: 'الرد في موضوع مرتبط t' + share_post: 's مشاركة المنشور' + reply_as_new_topic: 't الرد كموضوع مرتبط' reply_topic: 'shift+r الرد على الموضوع' - reply_post: 'r الرد على المشاركة' - quote_post: 'q اقتباس المشاركة' - like: 'l الإعجاب بالمشاركة' - flag: '! علم على المشاركة' - bookmark: 'b أضف مرجعية للمشاركة' - edit: 'e تعديل المشاركة' - delete: 'd حذف المشاركة' + reply_post: 'r الرد على المنشور' + quote_post: 'q اقتباس المنشور' + like: 'l الإعجاب بالمنشور' + flag: '! الإبلاغ عن المنشور' + bookmark: 'b وضع علامة مرجعية علي المنشور' + edit: 'e تعديل المنشور' + delete: 'd حذف المنشور' mark_muted: 'm، m كتم الموضوع' mark_regular: 'm, r موضوع منظم (الإفتراضي)' mark_tracking: 'm، t متابعة الموضوع' @@ -2618,7 +2668,7 @@ ar: sort_by_count: "العدد" sort_by_name: "الاسم" manage_groups: "أدر مجموعات الوسوم" - manage_groups_description: "اصنع مجموعات لتنظيم الوسوم" + manage_groups_description: "انشئ مجموعات لتنظيم الوسوم" filters: without_category: "مواضيع %{tag} %{filter}" with_category: "موضوعات %{filter}%{tag} في %{category}" @@ -2658,27 +2708,27 @@ ar: unread: "ليست هناك مواضيع غير مقروءة." new: "ليست هناك مواضيع جديدة." read: "لم تقرأ أيّ موضوع بعد." - posted: "لم تشارك في أيّ موضوع بعد." - latest: "ليست هناك مواضيع حديثة." - hot: "لا يوجد المزيد من المواضيع النشطة" - bookmarks: "لا مواضيع معلّمة بعد." - top: "لا يوجد المزيد من المواضيع العليا" - search: "لا نتائج للبحث." + posted: "لم تنشر في أيّ موضوع بعد.." + latest: "لا يوجد موضوعات حديثة." + hot: "لا يوجد موضوعات نشطة." + bookmarks: "لم تقم بوضع علامات مرجعية علي اي موضوع بعد." + top: "لا يوجد موضوعات الاكثر مشاهدة." + search: "لا يوجد نتائج للبحث." bottom: - latest: "ليست هناك مواضيع حديثة أخرى." - hot: "لا يوجد المزيد من المواضيع النشطة" - posted: "لا يوجد مواضيع أخرى." - read: "لا مواضيع مقروءة أخرى." - new: "لا مواضيع جديدة أخرى." - unread: "لا مواضيع غير مقروءة أخرى." - top: "لقد اطلعت على كل المواضيع المميزة حتى هذه اللحظة." - bookmarks: "لايوجد المزيد من المواضيع في المفضلة" + latest: "لا يوجد المزيد من الموضوعات الحديثة." + hot: "لا يوجد المزيد من الموضوعات النشطة." + posted: "لا يوجد المزيد من الموضوعات المنشورة." + read: "لا يوجد المزيد من الموضوعات المقروءة." + new: "لا يوجد المزيد من الموضوعات الجديدة." + unread: "لا يوجد المزيد من الموضوعات غير مقروءة." + top: "لا يوجد المزيد من الموضوعات الاكثر مشاهدة." + bookmarks: "لا يوجد المزيد من الموضوعات التي عليها علامة مرجعية." search: "لا نتائج بحث أخرى." invite: custom_message: "اجعل دعوتك شخصيّة أكثر بكتابة" custom_message_link: "رسالة مخصصة" custom_message_placeholder: "ادخل رسالتك المخصصة" - custom_message_template_forum: "مرحبا. عليك الانضمام إلى هذا المنتدى!" + custom_message_template_forum: "مرحبا. عليك الانضمام إلى هذا المجتمع!" custom_message_template_topic: "مرحبا. أظن أن هذا الموضوع سيسعدك!" safe_mode: enabled: "الوضع الآمن مفعّل، لتخرج منه أغلق نافذة المتصفّح هذه" @@ -2686,18 +2736,18 @@ ar: type_to_filter: "اكتب للتّرشيح..." admin: title: 'مدير المجتمع' - moderator: 'مراقب' + moderator: 'مشرف' dashboard: title: "لوحة التحكم" last_updated: "أخر تحديث للوحة التحكم:" - version: "الإصدارة" - up_to_date: "لديك أحدث إصدارة!" - critical_available: "يتوفّر تحديث لمشاكل حرجة." + version: "الإصدار" + up_to_date: "لديك أحدث إصدار!" + critical_available: "يتوفّر تحديث لمشكلات حرجة." updates_available: "التحديثات متوفرة." please_upgrade: "من فضلك رقّ البرمجية!" - no_check_performed: "لم يجرِ التماس التّحديثات. تحقّق من عمل sidekiq." - stale_data: "لم يجرِ التماس التّحديثات حديثًا. تحقّق من عمل sidekiq." - version_check_pending: "يبدو أنك رقّيت الموقع مؤخرا. مذهل!" + no_check_performed: "لم يتم التحقق من التحديثات. تأكد أن sidekiq قيد التشغيل." + stale_data: "لم يتم التحقق من التحديثات مؤخراً. تاكد أن sidekiq قيد التشغيل." + version_check_pending: "يبدو أنك قمت بتحديث الموقع مؤخرا. رائع!" installed_version: "المثبتة" latest_version: "الأخيرة" problems_found: "عُثر على مشاكل في تثبيت نسخة دسكورس هذه:" @@ -2706,12 +2756,12 @@ ar: no_problems: "لا مشاكل." moderators: 'المشرفون:' admins: 'المدراء:' - blocked: 'محظور:' - suspended: 'موقوف:' + blocked: 'محظورون:' + suspended: 'موقوفون:' private_messages_short: "الرسائل" private_messages_title: "الرسائل" mobile_title: "متنقل" - space_free: "{{size}} حرّ" + space_free: "{{size}} فارغ" uploads: "عمليات الرفع" backups: "النسخ الاحتياطية" traffic_short: "المرور" @@ -2729,38 +2779,38 @@ ar: 30_days_ago: "منذ ٣٠ يوم" all: "الكل" view_table: "جدول" - view_graph: "شكل رسومي" + view_graph: "رسم بياني" refresh_report: "تحديث التقرير " start_date: "تاريخ البدء" end_date: "تاريخ الإنتهاء" - groups: "جميع الفئات" + groups: "كل المجموعات" commits: latest_changes: "آخر تغيير: يرجى التحديث" by: "بواسطة" flags: - title: "التبليغات" - active_posts: "المشاركات المبلغ عنها " - old_posts: "المشاركات القديمة المبلغ عنها " - topics: "المواضيع المبلغ عنها " + title: "البلاغات" + active_posts: "المنشورات المبلغ عنها " + old_posts: "المنشورات القديمة المبلغ عنها " + topics: "الموشوعات المبلغ عنها " agree: "أوافق" - 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: "الموافقه على التبليغ" - agree_flag_title: "الموافقة مع التَعَلّيم وحفظ المشاركة دون تغيير." + 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: "الموافقه على البلاغ" + agree_flag_title: "الموافقة علي البلاغ و حفظ المنشور دون تغيير." defer_flag: "تأجيل" - defer_flag_title: "إزالة البلاغ، لا يتطلب منك إجراء في الوقت الحالي." - delete: "حذف" - delete_title: "حذف المشاركة المرتبطة بهذا البلاغ" - delete_post_defer_flag: "حذف المشاركة مع تأجيل البلاغ" - delete_post_defer_flag_title: "حذف المشاركة. اذا كانت المشاركة الاولى, احذف الموضوع" - delete_post_agree_flag: "حذف المشاركة مع الموافقة على البلاغ" - delete_post_agree_flag_title: "حذف المشاركة. اذا كانت المشاركة الاولى, احذف الموضوع" - delete_flag_modal_title: "حذف مع ..." - delete_spammer: "احذف ناشر السخام" + defer_flag_title: "ازل هذا البلاغ، لا يتطلب منك إجراء في الوقت الحالي." + delete: "احذف" + delete_title: "احذف المنشور المرتبط بهذا البلاغ" + delete_post_defer_flag: "احذف المنشور مع تأجيل البلاغ" + delete_post_defer_flag_title: "احذف المنشور. اذا كان المنشور الاول, احذف الموضوع" + delete_post_agree_flag: "احذف المنشور مع الموافقة على البلاغ" + delete_post_agree_flag_title: "احذف المنشور. اذا كان المنشور الاول, احذف الموضوع" + delete_flag_modal_title: "احذف و..." + delete_spammer: "احذف ناشر السبام" delete_spammer_title: "احذف المستخدم مع مشاركاته ومواضيعه." disagree_flag_unhide_post: "أختلف مع البلاغ، إعادة إظهار المشاركة." disagree_flag_unhide_post_title: "حذف أي بلاغ يخص هذه المشاركة مع إظهارها مرة أخرى" @@ -3061,11 +3111,17 @@ ar: text: "بعد الـ Header" footer: text: "تذييل " + title: "ادخل نص الـ HTML ليتم عرضه في ذيل الصفحة (الفوتر)" + embedded_scss: + text: "CSS مضمن" head_tag: text: "" body_tag: text: "" colors: + select_base: + title: "اختر مخطط الالوان الاساسي" + description: "مخطط اساسي" title: "الألوان" edit: "عدل مخططات الألوان" long_title: "مخططات الألوان" @@ -3283,11 +3339,16 @@ ar: censor: 'راقب (ضلل)' require_approval: 'يحتاج لموافقة' flag: 'علم' + action_descriptions: + require_approval: 'المنشورات التي تحتوي على هذه الكلمات تحتاج الى موافقة الطاقم قبل ان يتم رؤيتهم.' + flag: 'اسمح للمنشورات التي تحتوي على هذه الكلمات, ولكن علمهم كـ غير مناسب لكي يتمكن المشرف من مراجعتهم.' form: label: 'كلمة جديدة:' + placeholder_regexp: "تعبير اعتيادي" add: 'اضف' success: 'نجاح' upload: "حمل" + upload_successful: "تم الرفع بنجاح. تم اضافة الكلمات" impersonate: title: "انتحال الشخصية" help: "استخدم هذه الأداة لانتحال شخصية حساب مستخدم لأغراض التصحيح. سيتم تسجيل خروجك عندما تنتهي." @@ -3361,10 +3422,15 @@ ar: suspend_failed: "حدث خطب ما أثناء إيقاف هذا المستخدم {{error}}" unsuspend_failed: "حدث خطب ما أثناء إلغاء إيقاف هذا المستخدم {{error}}" suspend_duration: "ما المدّة التي سيُوقف هذا المستخدم خلالها؟" - suspend_duration_units: "(أيام)" suspend_reason_label: "لماذا هل أنت عالق؟ هذا النص سيكون ظاهراً للكل على صفحة تعريف هذا العضو, وسيكون ظاهراً للعضو عندما يحاول تسجل الدخول. احفظها قصيرة." + suspend_reason_hidden_label: "لماذا انت موقوف؟ هذا النص سيظهر للعضو حين يحاول الولوج. اجعله قصيراً." suspend_reason: "سبب" + suspend_reason_placeholder: "سبب التوقيف" + suspend_message: "رسالة بريد الكتروني" + suspend_message_placeholder: "اختياري. وفر المزيد من المعلومات حول التوقيف و سيتم ارساله عبر البريد الالكتروني الى العضو." suspended_by: "محظور من قبل" + suspended_until: "(حتى %{until})" + cant_suspend: "لا يمكن ايقاف هذا العضو" delete_all_posts: "احذف كل مشاركاته" delete_all_posts_confirm_MF: "أنت على وشك {POSTS, plural, zero {عدم حذف شيء} one {حذف مشاركة واحدة} two {حذف مشاركتين} few {حذف # مشاركات} many {حذف # مشاركة} other {حذف # مشاركة}}{TOPICS, plural, zero {} one { وموضوع واحد} two { وموضوعين} few { و# مواضيع} many {و# موضوعا} other {و# موضوع}}. أمتأكد؟" suspend: "علّق" @@ -3385,6 +3451,7 @@ ar: logged_out: "أخرجنا العضو من كلّ أجهزته" revoke_admin: 'سحب الإدارة' grant_admin: 'منحة إدارية' + grant_admin_confirm: "لقد ارسلنا بريداً الكترونياً لتأكيد المدير الجديد. رجاء افتح الرسالة و اتبع التعلميات." revoke_moderation: 'سحب المراقبة' grant_moderation: 'منحة مراقبة' unblock: 'إلغاء حظر' diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 340b50c267..822fb56727 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -540,6 +540,7 @@ el: admin_tooltip: "Αυτός ο χρήστης είναι διαχειριστής" blocked_tooltip: "Ο χρήστης είναι μπλοκαρισμένος" suspended_notice: "Αυτός ο χρήστης είναι σε αποβολή μέχρι τις {{date}}." + suspended_permanently: "Ο χρήστης είναι αποβλημένος." suspended_reason: "Αιτιολογία:" github_profile: "Github" email_activity_summary: "Περίληψη Ενεργειών" @@ -1415,7 +1416,9 @@ el: later_this_week: "Αργότερα αυτή την εβδομάδα" this_weekend: "Αυτό το Σαββατοκύριακο" next_week: "Την άλλη εβδομάδα" + two_weeks: "Δύο Εβδομάδες" next_month: "Τον άλλο μήνα" + forever: "Για Πάντα" pick_date_and_time: "Επίλεξε ημερομηνία και ώρα" set_based_on_last_post: "Κλείσε ανάλογα με την τελευταία ανάρτηση" publish_to_category: diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 647c13bf2c..d8bb0fdd29 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -540,6 +540,7 @@ fi: admin_tooltip: "Tämä käyttäjä on ylläpitäjä" blocked_tooltip: "Tämä käyttäjä on estetty" suspended_notice: "Tämä käyttäjätili on hyllytetty {{date}} asti." + suspended_permanently: "Käyttäjä on hyllytetty." suspended_reason: "Syy:" github_profile: "GitHub" email_activity_summary: "Kooste tapahtumista" @@ -1416,7 +1417,9 @@ fi: later_this_week: "Myöhemmin tällä viikolla" this_weekend: "Viikonloppuna" next_week: "Ensi viikolla" + two_weeks: "Kahden viikon kuluttua" next_month: "Ensi kuussa" + forever: "Ikuisesti" pick_date_and_time: "Valitse päivämäärä ja kellonaika" set_based_on_last_post: "Sulje viimeisimmän viestin mukaan" publish_to_category: @@ -2959,6 +2962,7 @@ fi: form: label: 'Uusi sana:' placeholder: 'kokonainen sana tai * jokerimerkkinä' + placeholder_regexp: "säännöllinen lauseke" add: 'Lisä' success: 'Onnistui' upload: "Lataa" @@ -3037,7 +3041,7 @@ fi: moderator: "Valvoja?" admin: "Ylläpitäjä?" blocked: "Estetty?" - staged: "Luotu?" + staged: "Esikäyttäjä?" show_admin_profile: "Ylläpito" refresh_browsers: "Pakota sivun uudelleen lataus" refresh_browsers_message: "Sanoma lähetettiin kaikille päätelaitteille!" @@ -3112,7 +3116,7 @@ fi: deactivate_explanation: "Käytöstä poistetun käyttäjän täytyy uudelleen vahvistaa sähköpostiosoitteensa." suspended_explanation: "Hyllytetty käyttäjä ei voi kirjautua sisään." block_explanation: "Estetty käyttäjä ei voi luoda viestejä tai ketjuja." - staged_explanation: "Automaattisesti luotu käyttäjä voi kirjoittaa vain tiettyihin ketjuihin sähköpostin välityksellä." + staged_explanation: "Esikäyttäjä voi kirjoittaa vain tiettyihin ketjuihin sähköpostin välityksellä." bounce_score_explanation: none: "Tästä sähköpostiosoitteesta ei ole tullut palautuksia viime aikoina" some: "Tästä sähköpostiosoitteesta on tullut joitakin palautuksia viime aikoina" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 8d244ee84a..0fc8215647 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -2369,8 +2369,9 @@ fr: by: "par" flags: title: "Signalements" - old: "Anciens" - active: "Actifs" + active_posts: "Messages signalés" + old_posts: "Anciens messages signalés" + topics: "Sujets signalés" agree: "Accepter" agree_title: "Confirmer que le signalement est valide et correcte" agree_flag_modal_title: "Accepter et…" @@ -2398,6 +2399,8 @@ fr: clear_topic_flags: "Terminer" clear_topic_flags_title: "Ce sujet a été étudié et les problèmes ont été résolus. Cliquez sur Terminer pour enlever les signalements." more: "(plus de réponses…)" + suspend_user: "Suspendre l'utilisateur" + suspend_user_title: "Suspendre l'utilisateur pour ce message" dispositions: agreed: "accepté" disagreed: "refusé" @@ -2408,11 +2411,18 @@ fr: system: "Système" error: "Quelque chose s'est mal passé" reply_message: "Répondre" - no_results: "Il n'y a aucun signalement." + no_results: "Il n'y a pas de messages signalés." topic_flagged: "Ce sujet a été signalé." + show_full: "afficher le message complet" visit_topic: "Consulter le sujet pour intervenir" was_edited: "Le message a été modifié après le premier signalement" previous_flags_count: "Ce message a déjà été signalé {{count}} fois." + show_details: "Afficher les détails du signalement" + flagged_topics: + topic: "Sujet" + type: "Type" + users: "Utilisateurs" + last_flagged: "Dernier signalés" summary: action_type_3: one: "hors sujet" @@ -2950,6 +2960,7 @@ fr: form: label: 'Nouveau mot :' placeholder: 'mot complet ou * comme signe générique' + placeholder_regexp: "expression régulière" add: 'Ajouter' success: 'Succès' upload: "Envoyer" @@ -3011,10 +3022,15 @@ fr: suspend_failed: "Il y a eu un problème pendant la suspension de cet utilisateur {{error}}" unsuspend_failed: "Il y a eu un problème pendant le retrait de la suspension de cet utilisateur {{error}}" suspend_duration: "Combien de temps l'utilisateur sera suspendu ?" - suspend_duration_units: "(jours)" suspend_reason_label: "Pourquoi suspendez-vous ? Ce texte sera visible par tout le monde sur la page du profil de cet utilisateur, et sera affiché à l'utilisateur quand ils essaient de se connecter. Soyez bref." + suspend_reason_hidden_label: "Pourquoi le suspendez-vous ? Ce texte sera affiché à l'utilisateur quand il essaie de se connecter. Soyez bref." suspend_reason: "Raison" + suspend_reason_placeholder: "Raison de la suspension" + suspend_message: "Message courriel" + suspend_message_placeholder: "Donner plus d'informations au sujet de la suspension qui seront envoyées à l'utilisateur par courriel." suspended_by: "Suspendu par" + suspended_until: "(jusqu'à %{until})" + cant_suspend: "Cet utilisateur ne peut pas être suspendu." delete_all_posts: "Supprimer tous les messages" delete_all_posts_confirm_MF: "Vous êtes sur le point de supprimer {POSTS, plural, one {1 message} other {# messages}} et {TOPICS, plural, one {1 sujet} other {# sujets}}. Êtes-vous sûr ?" suspend: "Suspendre" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index b9f0e3a01b..2321fddf5b 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -540,6 +540,7 @@ it: admin_tooltip: "Questo utente è un amministratore" blocked_tooltip: "Questo utente è bloccato" suspended_notice: "Questo utente è sospeso fino al {{date}}." + suspended_permanently: "Questo utente è sospeso." suspended_reason: "Motivo: " github_profile: "Github" email_activity_summary: "Riassunto Attività" @@ -1415,7 +1416,9 @@ it: later_this_week: "Più tardi questa settimana" this_weekend: "Questo fine settimana" next_week: "La prossima settimana" + two_weeks: "Due Settimane" next_month: "Il prossimo mese" + forever: "Per sempre" pick_date_and_time: "Scegli la data e l'ora" set_based_on_last_post: "Chiudi in base all'ultimo messaggio" publish_to_category: @@ -2956,6 +2959,7 @@ it: form: label: 'Nuova Parola:' placeholder: 'parola completa o * come carattere jolly' + placeholder_regexp: "espressione regolare" add: 'Aggiungi' success: 'Successo' upload: "Carica" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 6f2210b094..c2906f34b6 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -44,17 +44,17 @@ ja: less_than_x_seconds: other: "%{count}秒未満" x_seconds: - other: "%{count}秒前" + other: "%{count}秒" x_minutes: - other: "%{count}分前" + other: "%{count}分" about_x_hours: - other: "%{count}時間前" + other: "%{count}時間" x_days: - other: "%{count}日前" + other: "%{count}日" about_x_years: - other: "%{count}年前" + other: "%{count}年" over_x_years: - other: "%{count}年以上前" + other: "%{count}年以上" almost_x_years: other: "約%{count}年" date_month: "MMM Do" @@ -1630,6 +1630,8 @@ ja: yes_value: "はい" via_email: "メールで投稿されました。" whisper: "この投稿はモデレーターへのプライベートメッセージです" + wiki: + about: "この投稿はWiki形式です" archetypes: save: '保存オプション' controls: @@ -2160,6 +2162,8 @@ ja: clear_topic_flags: "完了" clear_topic_flags_title: "このトピックについての問題が解決されました。「完了」をクリックして通報の対応を完了します。" more: "(more replies...)" + suspend_user: "ユーザを凍結" + suspend_user_title: "この投稿においてユーザを凍結" dispositions: agreed: "賛成しました。" disagreed: "反対する" @@ -2170,7 +2174,7 @@ ja: system: "システム" error: "何らかの理由でうまくいきませんでした" reply_message: "返信する" - no_results: "通報はありません。" + no_results: "通報された投稿はありません。" topic_flagged: "この トピック は通報されました。" visit_topic: "トピックを確認" was_edited: "最初の通報後に編集された投稿" @@ -2284,6 +2288,8 @@ ja: without_uploads: "はい(ファイルは含まない)" download: label: "ダウンロード" + title: "ダウンロードリンクをメールで送る" + alert: "バックアップのダウンロードリンクがメールで送られました。" destroy: title: "バックアップを削除" confirm: "このバックアップを削除しますか?" @@ -2329,8 +2335,25 @@ ja: subject: "件名" multiple_subjects: "このメールのテンプレートは複数の件名があります。" none_selected: "編集するメールテンプレートを選択してください。" + revert: "変更を元に戻す" + theme: + edit: "編集" + desktop: "デスクトップ" + mobile: "モバイル" + preview: "プレビュー" + upload: "アップロード" + license: "ライセンス" + check_for_updates: "アップデートを確認" + updating: "アップデート中..." + scss: + text: "CSS" + head_tag: + text: "" + body_tag: + text: "" colors: title: "カラー" + edit: "カラースキームの編集" long_title: "カラースキーム" new_name: "カラースキームを作成" copy_name_prefix: "のコピー" @@ -2416,6 +2439,9 @@ ja: subject: "件名" body: "本文" filters: + from_placeholder: "from@example.com" + to_placeholder: "to@example.com" + cc_placeholder: "cc@example.com" subject_placeholder: "件名..." error_placeholder: "エラー" logs: @@ -2488,7 +2514,12 @@ ja: revoke_admin: "管理者権限を剥奪" grant_moderation: "モデレータ権限を付与" revoke_moderation: "モデレータ権限を剥奪" + backup_create: "バックアップを生成" + deleted_tag: "タグを削除" + renamed_tag: "タグ名変更" change_readonly_mode: "閲覧専用モードに変更する" + backup_download: "バックアップをダウンロード" + backup_destroy: "バックアップを消去" screened_emails: title: "ブロック対象アドレス" description: "新規アカウント作成時、次のメールアドレスからの登録をブロックする。" @@ -2529,6 +2560,7 @@ ja: flag: 'これらの言葉を含む投稿を許可しますが、仲裁者がこれをレビューできるよう、不適切であると通報されます。' form: label: '新しい単語:' + placeholder_regexp: "正規表現" add: '追加' success: '成功' upload: "アップロード" @@ -2586,9 +2618,9 @@ ja: suspend_failed: "ユーザの凍結に失敗しました: {{error}}" unsuspend_failed: "ユーザの凍結解除に失敗しました: {{error}}" suspend_duration: "ユーザを何日間凍結しますか?" - suspend_duration_units: "(日)" suspend_reason_label: "アカウントを凍結する理由を説明してください。ここに書いた理由は、このユーザのプロファイルページにおいて全員が閲覧可能な状態で公開されます。またこのユーザがログインを試みた際にも表示されます。" suspend_reason: "理由" + suspend_reason_placeholder: "凍結理由" suspended_by: "凍結したユーザ" delete_all_posts: "全ての投稿を削除" suspend: "凍結" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 62cf421b13..55f9b73024 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -345,6 +345,11 @@ ko: closed_group: 닫힌 그룹 is_group_user: "당신은 이 그룹의 구성원입니다." allow_membership_requests: "사용자가 그룹 소유자에게 가입신청을 할 수 있도록 허용합니다" + membership_request_template: "가입 요청을 전송할 때 사용자에게 표시할 커스텀 템플릿" + membership_request: + submit: "요청 보내기" + title: "@%{group_name}에 가입 요청하기" + reason: "그룹 소유자에게 왜 이 그룹에 속해야하는지 알립니다." membership: "회원제" name: "이름" user_count: "멤버 수" @@ -506,7 +511,8 @@ ko: admin_tooltip: "이 회원은 관리자입니다." blocked_tooltip: "이 회원은 차단되었습니다" suspended_notice: "이 회원은 {{date}}까지 접근 금지 되었습니다." - suspended_reason: "이유: " + suspended_permanently: "이 회원은 일시정지되었습니다." + suspended_reason: "사유: " github_profile: "Github" email_activity_summary: "활동 요약" mailing_list_mode: @@ -990,6 +996,7 @@ ko: alt: 'Alt' select_box: default_header_text: 선택... + no_content: 검색 결과가 없습니다 filter_placeholder: 검색... emoji_picker: filter_placeholder: 이모지 찾기 @@ -1154,6 +1161,8 @@ ko: searching: "검색중..." post_format: "#{{post_number}} by {{username}}" results_page: "검색 결과" + search_google_button: "Google" + search_google_title: "이 사이트 검색" context: user: "@{{username}}의 글 검색" category: "#{{category}} 카테고리에서 검색" @@ -1185,6 +1194,7 @@ ko: seen: 읽은 것 unseen: 읽지 않은 것 wiki: 은(는) 위키입니다. + images: 이미지 포함 all_tags: 모든 태그를 포함 statuses: label: 토픽 조건 @@ -2220,8 +2230,7 @@ ko: by: "에 의해" flags: title: "신고" - old: "지난" - active: "활성화된" + active_posts: "신고된 포스트" agree: "동의" agree_title: "이 신고가 올바르고 타당함을 확인합니다" agree_flag_modal_title: "동의 및 ..." @@ -2249,6 +2258,7 @@ ko: clear_topic_flags: "완료" clear_topic_flags_title: "주제 조사를 끝냈고 이슈를 해결했습니다. 신고를 지우기 위해 완료를 클릭하세요" more: "(더 많은 답글...)" + suspend_user: "정지된 사용자" dispositions: agreed: "동의" disagreed: "반대" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index c4c3d9ab62..28dad2f16d 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -540,6 +540,7 @@ nb_NO: admin_tooltip: "Denne brukeren er en administrator" blocked_tooltip: "Denne brukeren er blokkert" suspended_notice: "Denne brukeren er bannlyst til {{date}}." + suspended_permanently: "Brukeren er bannlyst." suspended_reason: "Begrunnelse:" github_profile: "Github" email_activity_summary: "Oppsummering av aktivitet" @@ -1409,13 +1410,17 @@ nb_NO: remove: "Fjern utløp" publish_to: "Publiser til:" when: "Når:" + public_timer_types: Emneutløp + private_timer_types: Brukerstyrte emneutløp auto_update_input: later_today: "Senere i dag" tomorrow: "I morgen" later_this_week: "Senere denne uken" this_weekend: "Denne uken" next_week: "Neste uke" + two_weeks: "To uker" next_month: "Neste måned" + forever: "For alltid" pick_date_and_time: "Velg dato og tid" set_based_on_last_post: "Lukk basert på siste post" publish_to_category: diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 566e8d232e..0262442353 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -2578,8 +2578,9 @@ pl_PL: by: "przez" flags: title: "Flagi" - old: "Stare" - active: "Aktywność" + active_posts: "Ofllagowane wpisy" + old_posts: "Stare oflagowane wpisy" + topics: "Oflagowane tematy" agree: "Potwierdź" agree_title: "Potwierdź to zgłoszenie jako uzasadnione i poprawne" agree_flag_modal_title: "Potwierdź i…" @@ -2607,6 +2608,8 @@ pl_PL: clear_topic_flags: "Zrobione" clear_topic_flags_title: "Ten temat został sprawdzony i związane z nim problemy zostały rozwiązane. Kliknij Zrobione, aby usunąć flagi." more: "(więcej odpowiedzi…)" + suspend_user: "Zawieszony użytkownik" + suspend_user_title: "Zawieś użytkownika za ten wpis" dispositions: agreed: "potwierdzono" disagreed: "wycofano" @@ -2617,11 +2620,18 @@ pl_PL: system: "System" error: "Coś poszło nie tak" reply_message: "Odpowiedz" - no_results: "Nie ma flag." + no_results: "Nie ma żadnych oflagowanych wpisów." topic_flagged: "Ten temat został oflagowany." + show_full: "pokaż cały wpis" visit_topic: "Odwiedź temat by podjąć działania." was_edited: "Wpis został zmieniony po pierwszej fladze" previous_flags_count: "Ten wpis został do tej pory oznaczony flagą {{count}} razy." + show_details: "Pokaż szczegóły oflagowania" + flagged_topics: + topic: "Temat" + type: "Typ" + users: "Użytkownicy" + last_flagged: "Ostatnio Oflagowane" summary: action_type_3: one: "nie-na-temat" @@ -3177,6 +3187,7 @@ pl_PL: form: label: 'Nowe słowo:' placeholder: 'pełne słowo lub jako dzika karta' + placeholder_regexp: "wyrażenie regularne" add: 'Dodaj' success: 'Sukces' upload: "Prześlij" @@ -3246,10 +3257,15 @@ pl_PL: suspend_failed: "Coś poszło nie tak podczas zawieszania użytkownika {{error}}" unsuspend_failed: "Coś poszło nie tak podczas odwieszania użytkownika {{error}}" suspend_duration: "Jak długo użytkownik ma być zawieszony?" - suspend_duration_units: "(dni)" suspend_reason_label: "Dlaczego zawieszasz? Ten tekst będzie widoczny dla wszystkich na stronie profilu użytkownika i będzie wyświetlany użytkownikowi gdy ten będzie próbował się zalogować. Zachowaj zwięzłość." + suspend_reason_hidden_label: "Dlaczego zawieszasz użytkownika? Ten krótki tekst zostanie wyświetlony, gdy zawieszony użytkownik spróbuje się zalogować. " suspend_reason: "Powód" + suspend_reason_placeholder: "Powód zawieszenia" + suspend_message: "Wiadomość Email" + suspend_message_placeholder: "Ewentualnie podaj więcej informacji o zawieszeniu użytkownika, a zostaną wysłane do niego poprzez email." suspended_by: "Zawieszony przez" + suspended_until: "(do %{until})" + cant_suspend: "Nie można zawiesić tego użytkownika." delete_all_posts: "Usuń wszystkie wpisy" delete_all_posts_confirm_MF: "Zamierzasz usunąć {POSTS, plural, one {1 post} few {# posty} many {# postów} other {# postów}} i {TOPICS, plural, one {1 temat} few {# tematy} many {# tematów} other {# tematów}}. Czy jesteś pewien?" suspend: "Zawieś" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 2938f52dff..72a04a7448 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -1446,6 +1446,8 @@ uk: change_trust_level: "зміна рівня довіри" change_username: "зміна імені користувача" change_site_setting: "зміна налаштування сайта" + change_theme: "змінити тему" + delete_theme: "видалити тему" change_site_text: "змінити текст сайту" suspend_user: "призупинення користувача" unsuspend_user: "скасування призупинення" @@ -1489,6 +1491,15 @@ uk: ip_address: "IP-адреса" add: "Додати" filter: "Пошук" + watched_words: + search: "Пошук" + clear_filter: "Очистити" + actions: + block: 'Заблокувати' + form: + add: 'Додати' + success: 'Успіх' + upload: "Вивантажити" impersonate: not_found: "Цього користувача не вдалося знайти." users: @@ -1496,6 +1507,7 @@ uk: create: 'Додати адміністратора' last_emailed: "Останній ел. лист" not_found: "Даруйте, такого імені користувача немає в нашій системі." + id_not_found: "Даруйте, користувача за таким номером немає в нашій системі." active: "Активний(а)" show_emails: "Показати електронну пошту" nav: diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index 2b6a86f7ce..0fea629d80 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -57,6 +57,8 @@ el: topic_closed_error: "Συμβαίνει όταν καταχωρηθεί μία απάντηση για θέμα που έχει κλείσει. " bounced_email_error: "Το email είναι αναφορά αποτυχίας παράδοσης. " screened_email_error: "Συμβαίνει όταν η διεύθυνση email του αποστολέα ήταν ήδη ελεγχόμενη" + unsubscribe_not_allowed: "Συμβαίνει όταν η απεγγραφή μέσω email δεν επιτρέπεται για αυτόν τον χρήστη." + email_not_allowed: "Συμβαίνει όταν η διεύθυνση email δεν βρίσκεται στην λευκή ή βρίσκεται στην μαύρη λίστα." unrecognized_error: "Άγνωστο Σφάλμα" errors: &errors format: '%{attribute} %{message}' @@ -979,6 +981,7 @@ el: gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF" enable_escaped_fragments: "Επιλογή του Ajax-Crawling API της Google, αν δεν βρεθεί κάποιο πρόγραμμα ανίχνευσης του Ιστού. Βλέπε https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Να επιτρέπεται στους συντονιστές να δημιουργούν νέες κατηγορίες" + crawler_user_agents: "Λίστα από user agents τα οποία θεωρούνται crawlers και τους παρέχεται static HTML αντί για JavaScript payload" cors_origins: "Επιτρεπόμενες πηγές για αιτήσεις πολλαπλής προέλευσης (cross-origin requests, CORS). Η κάθε προέλευση πρέπει να περιέχει http:// or https://. Η env μεταβλητή DISCOURSE_ENABLE_CORS πρέπει να οριστεί σε αληθινή για να ενεργοποιηθεί το CORS." use_admin_ip_whitelist: "Οι διαχειριστές μπορούν να συνδέονται μόνο αν βρίσκονται σε μια διευθυνση IP καθορισμένη στη λίστα Screened IPs (Admin > Logs > Screened Ips)." blacklist_ip_blocks: "Μια λίστα από private IP blocks η οποία ποτέ δεν θα ανιχνεύεται από το Discourse" @@ -1007,7 +1010,7 @@ el: allow_index_in_robots_txt: "Καθόρισε στο robots.txt ότι για αυτή την ιστοσελίδα επιτρέπεται να δημιουργείται κατάλογος περιεχομένων από τις μηχανές αναζήτησης." email_domains_blacklist: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες δεν μπορούν να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. Πχ: mailinator.com|trashmail.net" email_domains_whitelist: "Μία λίστα με διευθύνσεις email τις οποίες οι χρήστες ΘΑ ΠΡΕΠΕΙ να χρησιμοποιήσουν για να δημιουργήσουν λογαριασμό. ΠΡΟΣΟΧΗ: οι χρήστες με διευθύνσεις email οι οποίες δεν βρίσκονται σε αυτή τη λίστα δεν θα μπορούν να δημιουργήσουν λογαριασμό." - hide_email_address_taken: "Να μην ενημερώνονται οι χρήστες για την ύπαρξη ενός λογαριασμού όταν χρησιμοποιούν την λειτουργία ανάκτησης κωδικού πρόσβασης." + hide_email_address_taken: "Μην ενημερώνεις του χρήστες ότι υπάρχει λογαριασμός χρήστη με αυτήν την διεύθυνση email κατά την εγγραφή και στην φόρμα επαναφοράς κωδικού." log_out_strict: "Όταν αποσυνδεθείτε, ΟΛΕΣ οι δραστηριότητες σας σε ΟΛΕΣ τις συσκευές θα αποσυνδεθούν" version_checks: "Έλεγξε το Hub του Discourse για αναβαθμίσεις και δείξε μηνύματα για νέες ενημερώσεις στην σελίδα διαχείρισης. " new_version_emails: "Αποστολή email στην contact_email διεύθυνση όταν μια νέα έκδοση του Discourse είναι διαθέσιμη." @@ -1905,6 +1908,13 @@ el: Λυπούμαστε, αλλά το email σας προς %{destination} (με τίτλο %{former_title}) απέτυχε. Η απάντησή σας στάλθηκε από μία αποκλεισμένη διεύθυνση email. Δοκιμάστε να στείλετε από κάποια άλλη διεύθυνση email ή [επικοινωνήστε με την διαχείριση](%{base_url}/about). + email_reject_not_allowed_email: + title: "Email Απερρίφθη Μη Επιτρεπτό Email" + subject_template: "[%{email_prefix}] Πρόβλημα Email -- Αποκλεισμένο Email" + text_body_template: | + Λυπούμαστε, αλλά το email προς %{destination} (με τίτλο %{former_title}) δεν λειτούργησε. + + Η απάντησή σας στάλθηκε από μία αποκλεισμένη διεύθυνση email. Δοκιμάστε να στείλετε ξανά από κάποια άλλη διεύθυνση email ή [επικοινωνήστε με συνεργάτη](%{base_url}/about). email_reject_inactive_user: title: "Απόρριψη email Μη ενεργός χρήστης" subject_template: "[%{email_prefix}] Πρόβλημα Email -- Ανενεργός Χρήστης" @@ -2350,6 +2360,9 @@ el: %{reason} %{message} + account_exists: + title: "Ο λογαριασμός υπάρχει ήδη" + subject_template: "[%{email_prefix}] Ο λογαριασμός υπάρχει ήδη" digest: why: "Μια σύντομη σύνοψη του %{site_link} από την τελευταία σου επίσκεψη στις %{last_seen_at}" since_last_visit: "Από την τελευταία σου επίσκεψη" diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 7b810f186e..317dfad4bf 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -40,7 +40,7 @@ fi: incoming: default_subject: "Tämä ketju tarvitsee otsikon" show_trimmed_content: "Näytä piilotettu sisältö" - maximum_staged_user_per_email_reached: "Saavutti maksimimäärän automaattisesti luotuja tunnuksia per sähköpostiosoite" + maximum_staged_user_per_email_reached: "Saavutti maksimimäärän automaattisesti luotuja esikäyttäjiä per sähköpostiosoite." errors: empty_email_error: "Näin käy, kun saapuneessa sähköpostissa ei lue mitään." no_message_id_error: "Näin käy, kun viestin otsikkotiedoista puuttuu ID-tunniste (engl. message-ID)." @@ -57,6 +57,7 @@ fi: topic_closed_error: "Näin käy, kun vastauksen saapuessa ketju, johon viesti oli tarkoitettu, on suljettu." bounced_email_error: "Sähköposti on palautetun sähköpostin raportti" screened_email_error: "Näin käy, kun lähettäjän sähköpostiosoite on jo seulottu." + email_not_allowed: "Näin käy, kun sähköpostiosoite ei ole sallittujen listalla tai on kiellettyjen listalla." unrecognized_error: "Tuntematon virhe" errors: &errors format: '%{attribute} %{message}' @@ -567,7 +568,7 @@ fi: confirmed: "Sähköpostiosoite päivitetty." please_continue: "Jatka sivustolle %{site_name}" error: "Sähköpostiosoitteen vaihdossa tapahtui virhe. Ehkäpä tämä sähköpostiosoite on jo käytössä?" - error_staged: "Sähköpostiosoitetta muutettaessa tapahtui virhe. Osoite on automaattisesti luodun käyttäjän käytössä." + error_staged: "Sähköpostiosoitetta muutettaessa tapahtui virhe. Osoite on automaattisesti luodun esikäyttäjän käytössä." already_done: "Pahoittelut, tämä varmennuslinnkki ei ole enää voimassa. Ehkäpä sähköpostiosoitteesi on jo vaihdettu?" authorizing_old: title: "Kiitos sähköpostiosoitteesi varmentamisesta" @@ -635,6 +636,9 @@ fi: short_description: 'Äänestä viestiä' long_form: 'viestiä äänestetty' user_activity: + no_default: + self: "Et ole vielä tehnyt mitään mainittavaa." + others: "Ei ole tehnyt mitään mainittavaa." no_bookmarks: self: "Kirjanmerkeissäsi ei ole viestejä. Kirjanmerkit auttavat löytämään viestejä helposti myöhemmin." others: "Ei kirjanmerkkejä." @@ -965,6 +969,7 @@ fi: gtm_container_id: "Google Tag Manager -säiliön ID. Esim: GTM-ABCDEF" enable_escaped_fragments: "Käytä Googlen Ajax-sivustoille tarkoitettua API:a, jos webcrawleria ei tunnisteta. Katso https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Salli valvojien luoda uusia alueita" + crawler_user_agents: "Lista käyttäjäagenteista, joita pidetään hakurobotteina ja joille näytetään staattinen HTML-sivu JavaScript-tietosisällön sijaan" cors_origins: "Salli lähteet CORS-pyynnöille (cross-origin request). Jokaisen lähteen pitää sisältää http:// tai https://. DISCOURSE_ENABLE_CORS asetus pitää olla valittuna ottaaksesi CORSin käyttöön." use_admin_ip_whitelist: "Ylläpitäjät voivat kirjautua vain IP-osoitteista, jotka on määritetty Seulottavien IP:iden listassa (Ylläpito > Lokit > Seulottavat IP:t)" blacklist_ip_blocks: "Lista yksityisistä IP-blokeista, joita Discoursen ei tule käydä läpi" @@ -991,7 +996,7 @@ fi: allow_index_in_robots_txt: "Määrittele robots.txt-tiedostossa, että hakukoneet saavat luetteloida sivuston." email_domains_blacklist: "Pystyviivalla eroteltu lista sähköposti-verkkotunnuksista, joista käyttäjät eivät voi luoda tiliä. Esimerkiksi mailinator.com|trashmail.net" email_domains_whitelist: "Pystyviivalla eroteltu lista sähköposti-verkkotunnuksista, joista käyttäjien pitää luoda tilinsä. VAROITUS: Käyttäjiä, joiden sähköpostiosoite on muusta verkkotunnuksesta ei sallita!" - hide_email_address_taken: "Älä paljasta tilin olemassaoloa unohtuneen salasanan kyselyssä." + hide_email_address_taken: "Älä kerro käyttäjälle, että käyttäjätili annetulla sähköpostiosoitteella on jo olemassa, kun hän liittyy palstalle tai kun hän pyytää salasanan palauttamista." log_out_strict: "Kun kirjaudutaan ulos, kirjaa käyttäjä ulos kaikilta laitteilta" version_checks: "Pingaa Discourse Hubia päivityksistä ja näytä viesti /admin hallintapaneelissa kun uusi versio on saatavilla" new_version_emails: "Lähetä sähköposti contact_email osoitteeseen kun uusi versio Discoursesta on saatavilla." @@ -1093,6 +1098,7 @@ fi: allow_all_attachments_for_group_messages: "Salli kaikki sähköpostiliitteet ryhmäviesteissä." png_to_jpg_quality: "Muunnetun JPG-tiedoston laatu (1 on huonoin laatu, 99 on paras laatu, 100 ottaa pois käytöstä)." allow_staff_to_upload_any_file_in_pm: "Salli henkilökunnan ladata minkätyyppisiä liitteitä tahansa yksityisviesteihin." + strip_image_metadata: "Poista metadata kuvista." enable_flash_video_onebox: "Ota käyttöön swf- ja flv-linkkien (Adobe Flash) onebox-tuki. VAROITUS: saattaa lisätä tietoturvariskejä." default_invitee_trust_level: "Oletus luottamustaso (0-4) kutsutuille käyttäjille." default_trust_level: "Uusien käyttäjien oletusarvoinen luottamustaso (0-4). VAROITUS! Tämän muuttaminen altistaa roskapostille." @@ -1125,6 +1131,7 @@ fi: min_trust_to_edit_post: "Viestin muokkaamiseen vaadittava luottamustaso." min_trust_to_allow_self_wiki: "Minimiluottamustaso, jolla käyttäjä voi tehdä omasta viestistään wiki-viestin." min_trust_to_send_messages: "Yksityisviestien luomiseen vaadittava luottamustaso" + min_trust_to_send_email_messages: "Vähimmäisluottamustaso, jotta voi lähettää uusia yksityisviestejä sähköpostilla (esikäyttäjille)" newuser_max_links: "Kuinka monta linkkiä uusi käyttäjä voi lisätä viestiin." newuser_max_images: "Kuinka monta kuvaa uusi käyttäjä voi lisätä viestiin." newuser_max_attachments: "Kuinka monta liitettä uusi käyttäjä voi lisätä viestiin." @@ -1199,8 +1206,8 @@ fi: unsubscribe_via_email_footer: "Liitä sähköpostiviestien alaosaan mailto: linkki, jonka avulla saaja voi lakkauttaa sähköposti-ilmoitukset" delete_email_logs_after_days: "Poista sähköpostilokit (N) päivän jälkeen. Aseta 0 säilyttääksesi ikuisesti." max_emails_per_day_per_user: "Käyttäjälle päivässä lähetettävien sähköpostien enimmäismäärä. Aseta 0, jos et halua rajoittaa." - enable_staged_users: "Luo automaattisesti käyttäjiä, kun saapuvia sähköposteja käsitellään." - maximum_staged_users_per_email: "Maksimimäärä automaattisesti luotavia tilejä, kun käsitellään saapuvaa sähköpostia." + enable_staged_users: "Luo automaattisesti esikäyttäjiä, kun saapuvia sähköposteja käsitellään." + maximum_staged_users_per_email: "Enimmäismäärä automaattisesti luotuja esikäyttäjiä, kun käsitellään saapuvaa sähköpostia." auto_generated_whitelist: "Lista sähköpostiosoitteista, joiden viestejä ei tarkasteta automaattisesti luodun sisällön osalta. Esimerkki: foo@bar.com|discourse@bar.com" block_auto_generated_emails: "Estä saapuvat sähköpostit, jotka tunnistetaan automaattisesti luoduiksi." ignore_by_title: "Jätä sähköpostit huomiotta niiden otsikon perusteella." @@ -1260,6 +1267,7 @@ fi: anonymous_posting_min_trust_level: "Anonyymin tilan käyttämiseen vaadittava luottamustila" anonymous_account_duration_minutes: "Suojellaksesi anonymiteettiä, luo uusi anonyymi tili N minuutin välein jokaiselle käyttäjälle. Esimerkki: jos arvoksi asetetaan 600, kun 600 minuuttia tulee kuluneeksi edellisestä viestistä JA käyttäjä vaihtaa anonyymiin tilaan, luodaan uusi anonyymi tili." hide_user_profiles_from_public: "Älä näytä käyttäjäkortteja, käyttäjäprofiileita tai käyttäjähakemistoa kirjautumattomille käyttäjille." + hide_suspension_reasons: "Älä näytä hyllytysten syitä julkisesti käyttäjäprofiileissa." user_website_domains_whitelist: "Käyttäjän kotisivu voi olla näiden verkkotunnusten alainen. Pystyviivoin erotettu lista." allow_profile_backgrounds: "Salli käyttäjien ladata profiilin taustakuva." sequential_replies_threshold: "Kuinka monen peräkkäisen viestin jälkeen yhdessä ketjussa käyttäjää muistutetaan peräkkäisistä vastauksista." @@ -1276,6 +1284,7 @@ fi: topic_page_title_includes_category: "Ketjusivu sisältää alueen nimen." native_app_install_banner: "Tarjoaa toistuvasti vieraileville Discoursen käyttöjärjestelmäkohtaista sovellusta." share_anonymized_statistics: "Julkaise yksilöimättömät käyttötilastot." + auto_handle_queued_age: "Käsittele automaattisesti asiat, jotka ovat odottaneet käsittelyä näin monta päivää. Lippuja lykätään. Jonossa olevat viestit ja käyttäjät hylätään. Jos asetat 0:ksi, ominaisuus ei ole käytössä." max_prints_per_hour_per_user: "Tulostuspyyntöjen (/print) enimmäismäärä (aseta 0 poistaaksesi käytöstä)" full_name_required: "Koko nimi on käyttäjäprofiilin vaadittu kohta" enable_names: "Näytä käyttäjän koko nimi profiilissa, käyttäjäkortissa ja sähköposteissa. Poista käytöstä piilottaaksesi koko nimen kaikkialla." @@ -1307,6 +1316,7 @@ fi: auto_close_topics_post_count: "Maksimimäärä viestejä ketjussa, kunnes se suljetaan automaattisesti (0 poistaaksesi käytöstä)" code_formatting_style: "Viestikentän koodipainike käyttää oletuksena tätä koodimuotoilutyyliä" max_allowed_message_recipients: "Kuinka monta vastaanottajaa yksityisviestillä voi olla." + watched_words_regular_expressions: "Tarkkaillut sanat ovat säännöllisiä lausekkeita." default_email_digest_frequency: "Kuinka usein käyttäjille lähetetään sähköpostikooste oletuksena." default_include_tl0_in_digests: "Sisällytä uusien käyttäjien viestit sähköpostikoosteisiin oletuksena. Tätä voi muuttaa käyttäjäasetuksissa." default_email_private_messages: "Lähetä oletuksena sähköposti, kun joku lähettää käyttäjälle viestin." @@ -1377,6 +1387,8 @@ fi: invalid_regex: "Säännöllinen lauseke ei kelpaa tai ei ole sallittu." email_editable_enabled: "Asetus 'email editable' on otettava pois käytöstä ennen tämän asetuksen käyttöönottoa." enable_sso_disabled: "Asetus 'enable sso' on otettava käyttöön ennen tämän asetuksen käyttöönottoa." + staged_users_disabled: "\"Esikäyttäjät\" on otettava käyttöön ennen tämän asetuksen käyttöönottoa." + reply_by_email_disabled: "Asetus \"reply by email\" täytyy ottaa käyttöön ennen tämän asetuksen käyttöönottoa." search: within_post: "#%{post_number} käyttäjältä %{username}" types: @@ -1521,6 +1533,7 @@ fi: max_new_accounts_per_registration_ip: "Rekisteröitymisiä ei oteta vastaan IP-osoitteestasi (maksimimäärä saavutettu). Ota yhteyttä henkilökuntaan." website: domain_not_allowed: "Verkkosivu ei kelpaa. Sallitus verkkotunnukset ovat: %{domains}" + auto_rejected: "Hylättiin automaattisesti iän perusteella. Katso auto_handle_queued_age -sivustoasetus." flags_reminder: flags_were_submitted: one: "Viestejä liputettiin yli tunti sitten. [Tarkastele niitä](/admin/flags)." @@ -1875,6 +1888,13 @@ fi: Vastauksesi lähetettiin estetystä sähköpostiosoitteesta. Yritä lähettää viesti toisesta sähköpostiosoitteesta tai [ota yhteyttä henkilökuntaan](%{base_url}/about). + email_reject_not_allowed_email: + title: "Sähköposti hylätty - osoite ei sallittu" + subject_template: "[%{email_prefix}] Sähköpostiongelma -- Estetty osoite" + text_body_template: | + Pahoittelemme, mutta sähköpostin lähettäminen tänne %{destination} (otsikolla %{former_title}) ei onnistunut. + + Vastauksesi on peräisin estetystä sähköpostiosoitteesta. Kokeile lähettää toisesta sähköpostiosoitteesta tai [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_inactive_user: title: "Sähköposti hylätty - aktivoimaton käyttäjä" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Aktivoimaton käyttäjä" @@ -1923,9 +1943,17 @@ fi: email_reject_invalid_access: title: "Sähköposti hylätty - pääsy estetty" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Ei pääsyoikeutta" + text_body_template: | + Pahoittelut, sähköpostiviestiäsi tänne: %{destination} (otsikolla %{former_title}) ei voitu toimittaa. + + Käyttäjätililläsi ei ole oikeutta luoda ketjua sille alueelle. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_strangers_not_allowed: title: "Sähköposti hylätty - vierailla ei pääsyä" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Ei pääsyoikeutta" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Alueelle jolle lähetit viestin voivat kirjoittaa ne, joilla on käypä käyttäjätunnus ja sähköpostiosoite. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_invalid_post: title: "Sähköposti hylätty - viesti ei kelpaa" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Lähetysvirhe" @@ -1954,18 +1982,41 @@ fi: email_reject_reply_key: title: "Sähköposti hylätty - vastausavain" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Tuntematon vastausavain" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (titled %{former_title}) ei onnistunut. + + Sähköpostiviestin vastaustunniste, engl. 'reply key', ei ole kelvollinen, minkä vuoksi ei tiedetä, mihin asiaan viestisi oli tarkoitus vastata. [Ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_bad_destination_address: title: "Sähköposti hylätty - tuntematon vastaanottajaosoite" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Tuntematon Vastaanottaja: -osoite" email_reject_topic_not_found: title: "Sähköposti hylätty - ketjua ei löytynyt" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Ketjua ei löytynyt" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Ketjua johon yritit kirjoittaa ei ole enää olemassa -- ehkä se poistettiin? Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_topic_closed: title: "Sähköposti hylätty - ketju suljettu" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Suljettu ketju" + text_body_template: | + Pahoittelut, sähköpostiviestiäsi tänne: %{destination} (otsikolla %{former_title}) ei voitu toimittaa. + + Ketju, johon yritit vastata on tällä hetkellä suljettu, eikä siihen voi enää vastata. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). email_reject_auto_generated: title: "Sähköposti hylätty - automaattivastaus" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Automaattivastaus" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Järjestelmä havaitsi viestisi olevan tietokoneen automaattisesti luoma eikä ihmisen kirjoittama, eikä viestiä voitu siksi hyväksyä. Jos uskot, että tämä johtuu virheestä, [ota yhteyttä henkilökuntaan](%{base_url}/about). + email_reject_unrecognized_error: + title: "Sähköposti hylätty - Tunnistamaton virhe" + subject_template: "[%{email_prefix}] Sähköpostiongelma -- Tunnistamaton virhe" + text_body_template: | + Pahoittelut, sähköpostiviestisi lähetys kohteeseen %{destination} (otsikolla %{former_title}) ei onnistunut. + + Viestiäsi käsiteltäessä tapahtui tunnistamaton virhe eikä sitä siksi julkaistu. Kokeile uudelleen tai [ota yhteyttä henkilökuntaan](%{base_url}/about). email_error_notification: title: "Ilmoitus sähköpostivirheestä" subject_template: "[%{email_prefix}] Sähköpostiongelma -- virhe POP-autentikoinnissa" @@ -2068,6 +2119,15 @@ fi: Palkinto myönnetään vain kahdelle käyttäjälle joka kuukausi, ja se näkyy pysyvästi [käyttäjäsivullasi](%{base_url}/my/badges). Sinusta on äkkiä tullut tärkeä osa yhteisöä. Kiitos kun liityit, ja jatka samaa rataa! + queued_posts_reminder: + title: "Muistutukset jonossa olevista viesteistä" + subject_template: + one: "Yksi viesti odottaa tarkastusta" + other: "%{count} viestiä odottaa tarkastusta" + text_body_template: | + Hei + + Uusien käyttäjien viestejä odottaa valvojan hyväksyntää. [Hyväksy tai hylkää ne täällä](%{base_url}/queued-posts). unsubscribe_link: | Jos et enää halua näitä viestejä, [klikkaa tästä](%{unsubscribe_url}). unsubscribe_link_and_mail: | @@ -2093,6 +2153,8 @@ fi: visit_link_to_respond: "[Vieraile ketjussa](%{base_url}%{url}) vastataksesi." visit_link_to_respond_pm: "[Vieraile ketjussa](%{base_url}%{url}) vastataksesi." posted_by: "Käyttäjältä %{username} %{post_date}" + user_invited_to_private_message_pm_group: + title: "Ryhmä kutsuttiin yksityiskeskusteluun" user_invited_to_private_message_pm: title: "Käyttäjä kutsuttiin yksityiskeskusteluun" subject_template: "[%{email_prefix}] %{username} kutsui sinut yksityiskeskusteluun '%{topic_title}'" @@ -2225,6 +2287,26 @@ fi: text_body_template: |2 %{message} + account_suspended: + title: "Tili hyllytetty" + subject_template: "[%{email_prefix}] Tilisi on hyllytetty" + text_body_template: | + Sinut hyllytettiin palstalta %{suspended_till} asti. + + %{reason} + + %{message} + account_exists: + title: "Tili on jo olemassa" + subject_template: "[%{email_prefix}] TIli on jo olemassa" + text_body_template: | + Yritit luoda tilin sivustolle %{site_name} tai yritit muuttaa tilin sähköpostiosoitteeksi %{email}. Sähköpostiosoitteella %{email} on kuitenkin jo tili olemassa. + + Jos unohdit salasanasi, [voit uusia sen nyt](%{base_url}/password-reset). + + Jos et yrittänyt luoda tunnusta sähköpostiosoitteella %{email} tai vaihtaa sähköpostiosoitettasi, älä huoli - voit huoletta jättää tämän viestin huomiotta. + + Jos sinulla on kysyttävää, [ota yhteyttä avuliaaseen henkilökuntaamme](%{base_url}/about). digest: why: "Lyhyt kooste siitä mitä on tapahtunut sivustolla %{site_link} viimeisimmän vierailusi jälkeen %{last_seen_at}." since_last_visit: "Viime vierailusi jälkeen" @@ -2341,6 +2423,10 @@ fi: see_more: "Lisää" search_title: "Etsi tältä sivustolta" search_google: "Google" + login_required: + welcome_message: | + ## [Tervetuloa sivustolle %{title}](#welcome) + Käyttäjätili tarvitaan. Luo tili tai kirjaudu sisään. terms_of_service: title: "Käyttöehdot" signup_form_message: 'Olen lukenut ja ymmärtänyt Käyttöehdot.' @@ -2806,6 +2892,18 @@ fi: description: Erinomaista osallistumista ensimmäisen kuukauden aikana long_description: | Tämä ansiomerkki myönnetään kahdelle uudelle käyttäjälle joka kuukausi kiitoksena erinomaisesta osallistumisestaan. Mittari on tykkäykset: kuinka usein viesteistä tykätään ja kuka tykkää. + enthusiast: + name: Intoilija + description: Vieraili 10 päivänä + long_description: Tämä ansiomerkki myönnetään, kun olet vieraillut 10 peräkkäisenä päivänä. Kiitos kun olet ollut kanssamme yli viikon ajan! + aficionado: + name: Hullaantunut + description: Vieraili 100 päivänä + long_description: Tämä ansiomerkki myönnetään, kun olet vieraillut 100 peräkkäisenä päivänä. Sehän on yli kolme kuukautta! + devotee: + name: Omistautunut + description: Vieraili 365 päivänä + long_description: Tämä ansiomerkki myönnetään, kun olet vieraillut 365 peräkkäisenä päivänä. Vau, kokonainen vuosi! badge_title_metadata: "%{display_name} -ansiomerkki sivustolla %{site_title}" admin_login: success: "Sähköposti lähetetty" @@ -2890,6 +2988,7 @@ fi: description: "Sinun tai organisaatiosi yleinen yhteydenottosivu. Näytetään Tietoja-sivulla." site_contact: label: "Automaattiset viestit" + description: "Tämän käyttäjän nimissä Discourse lähettää käyttäjille kaikki automaattiset yksityisviestit, kuten liputusvaroitukset ja ilmoitukset valmistuneista varmuuskopioista." corporate: title: "Organisaatio" description: "Nämä nimet näkyvät rekisteriselosteen ja käyttöehtojen yhteydessä, joita voit milloin vain muokata henkilökunta-alueella. Jos taustalla ei ole yritystä, voit hypätä tämän vaiheen yli toistaiseksi." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 2973c69013..0fc8aacbdc 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -57,6 +57,8 @@ it: topic_closed_error: "Succede quando arriva una risposta ma l'argomento relativo è stato chiuso." bounced_email_error: "L'email rappresenta un rapporto di email tornata indietro." screened_email_error: "Succede quando l'indirizzo email del mittente era stato già vagliato." + unsubscribe_not_allowed: "Accade quando annullare l'iscrizione tramite email non è consentito per questo utente." + email_not_allowed: "Accade quando l'indirizzo email non è in whitelist o è nella blacklist." unrecognized_error: "Errore Non Riconosciuto" errors: &errors format: '%{attribute} %{message}' @@ -972,6 +974,7 @@ it: gtm_container_id: "Container id di Google Tag Manager. Es: GTM-ABCDEF" enable_escaped_fragments: "Usa le Ajax-Crawling API di Google se non viene rilevato un webcrawler. Vedi https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "Permetti ai moderatori di creare nuove categorie" + crawler_user_agents: "Elenco degli user agent che sono considerati crawler e hanno servito l'HTML statico anziché il payload di JavaScript" cors_origins: "Origini permesse per richieste cross-origin (CORS). Ogni origine deve includere http:// o https://. La variabile d'ambiente DISCOURSE_ENABLE_CORS deve essere impostata come true per abilitare CORS." use_admin_ip_whitelist: "Gli amministratori possono connettersi soltanto se hanno un indirizzo IP definito nell'elenco degli IP scansionati (Amministrazione > Log > IP scansionati). " blacklist_ip_blocks: "Un elenco di blocchi di IP privati che non dovrebbero mai essere scansionati da Discourse" @@ -1000,7 +1003,7 @@ it: allow_index_in_robots_txt: "Specifica nel file robots.txt che questo sito permette l'indicizzazione da parte dei motori di ricerca." email_domains_blacklist: "Una lista di domini email separati dal carattere pipe \"|\" con cui gli utenti non possono registrare un account. Ad esempio: mailinator.com/trashmail.net" email_domains_whitelist: "Una lista di domini email delimitati da carattere pipe (|) che gli utenti DEVONO usare per poter registrare i propri account. ATTENZIONE: gli utenti con domini email differenti da questi non saranno accettati!" - hide_email_address_taken: "Non informare gli utenti dell'esistenza o meno dell'account quando richiamano la funzione per la password dimenticata." + hide_email_address_taken: "Durante l'iscrizione e nel modulo per la password dimenticata, non informare gli utenti che già esiste un account con un dato indirizzo email." log_out_strict: "Quando ci si disconnette, esci da TUTTE le sessioni dell'utente su tutti i dispositivi" version_checks: "Verifica su Discourse Hub l'esistenza di aggiornamenti e mostra i messaggi per le nuove versioni nel cruscotto /admin" new_version_emails: "Invia un'email all'indirizzo contact_email quando è disponibile una nuova versione di Discourse." @@ -1243,7 +1246,7 @@ it: email_site_title: "Il titolo del sito utilizzato come mittente delle email dal sito. Impostazione di default a 'titolo' se non impostata. Se il tuo 'titolo' contiene caratteri non consentiti nelle stringhe del mittente email, utilizza questa impostazione." find_related_post_with_key: "Utilizza solo la 'reply key' per trovare il messaggio inviato. ATTENZIONE: la disattivazione di questa funzione consente di impersonare l'utente in base all'indirizzo email." minimum_topics_similar: "Quanti argomenti devono esistere prima di presentare argomenti simili quando si creano nuovi argomenti." - relative_date_duration: "Dopo quanti giorni dalla pubblicazione le date verranno mostrate in termini relativi (7g) anziché assoluti (20 feb)." + relative_date_duration: "Dopo quanti giorni dalla pubblicazione le date verranno mostrate in termini assoluti (20 feb) anziché relativi (7g)." delete_user_max_post_age: "Non permettere di cancellare utenti il cui primo messaggio è più vecchio di (x) giorni." delete_all_posts_max: "Numero massimo di messaggi che possono essere eliminati contemporaneamente con il pulsante Cancella Tutti. Se un utente ha un numero maggiore di tali messaggi, questi non possono essere eliminati contemporaneamente e l'utente non può essere cancellato." username_change_period: "Numero di giorni dall'iscrizione dopo i quali è possibile modificare il nome utente (imposta a 0 per non permettere il cambio del nome utente)." @@ -1323,6 +1326,7 @@ it: auto_close_topics_post_count: "Numero massimo di messaggi consentiti in un argomento prima di essere automaticamente chiuso (0 per disabilitare)" code_formatting_style: "Il pulsante del testo preformattato nel composer verrà predefinito a questo stile di formattazione del codice" max_allowed_message_recipients: "Numero massimo di destinatari consentiti in un messaggio privato." + watched_words_regular_expressions: "Le parole osservate sono espressioni regolari." default_email_digest_frequency: "Con quale frequenza gli utenti ricevono email riepilogative di default." default_include_tl0_in_digests: "Per impostazione predefinita, includi i messaggi dei nuovi utenti nelle email riepilogative. Gli utenti possono modificare questa impostazione nelle loro preferenze" default_email_private_messages: " Invia una email quando qualcuno scrive un messaggio ad un utente di default." @@ -1895,6 +1899,13 @@ it: text_body_template: | Spiacenti, ma il tuo messaggio email per %{destination} (intitolato %{former_title}) non ha funzionato. + La tua risposta è stata inviata da un indirizzo email bloccato. Prova a rispondere da un altro indirizzo email, o [contatta un membro dello staff](%{base_url}/about). + email_reject_not_allowed_email: + title: "Email Rifiuta Email Non Consentita" + subject_template: "[%{email_prefix}] Problema email -- Email Bloccata" + text_body_template: | + Spiacenti, ma il tuo messaggio email per %{destination} (intitolato %{former_title}) non ha funzionato. + La tua risposta è stata inviata da un indirizzo email bloccato. Prova a rispondere da un altro indirizzo email, o [contatta un membro dello staff](%{base_url}/about). email_reject_inactive_user: title: "Email Rifiutata Utente Inattivo" @@ -2337,6 +2348,17 @@ it: %{reason} %{message} + account_exists: + title: "L'account esiste già" + subject_template: "[%{email_prefix}] L'account esiste già" + text_body_template: | + Hai appena cercato di creare un account su %{site_name}, o hai tentato di cambiare l'email di un account su %{email}. Tuttavia, un account esiste già per %{email}. + + Se hai dimenticato la tua password, [reimpostala adesso](%{base_url}/password-reset). + + Se non hai cercato di creare un account per %{email} o di cambiare il tuo indirizzo email, non ti preoccupare – puoi tranquillamente ignorare questo messaggio. + + Se hai delle domande, [contatta il nostro amichevole staff](%{base_url}/about). digest: why: "Un breve sommario di %{site_link} dalla tua ultima visita il %{last_seen_at}" since_last_visit: "Dalla tua ultima visita" diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 2ed4faeb49..784d9d9ffc 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -703,12 +703,16 @@ pl_PL: short_description: 'Zagłosuj na ten wpis' long_form: 'zagłosowano za tym wpisem' user_activity: + no_default: + others: "Brak aktywności" no_bookmarks: self: "Nie masz postów dodanych do zakładek, posty w zakładkach pozwalają ci na łatwiejszy dostęp do nich w późniejszym czasie." others: "Brak zakładek." no_likes_given: self: "Nie masz lajkowanych postów." others: "Brak lajkowanych postów." + no_replies: + others: "Brak odpowiedzi." topic_flag_types: spam: title: 'Spam' diff --git a/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml b/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml index 1b2a4b6c34..f97a347f3b 100644 --- a/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml +++ b/plugins/discourse-narrative-bot/config/locales/client.tr_TR.yml @@ -9,5 +9,5 @@ tr_TR: js: discourse_narrative_bot: welcome_post_type: - new_user_track: "Yeni başlayan kullanıcılar için öğretici adımları başlatın" + new_user_track: "Tüm yeni başlayan kullanıcılar için yeni kullanıcı öğreticisini başlatın" welcome_message: "Hızlı başlangıç rehberi ile tüm yeni kullanıcılara hoş geldin mesajı gönder" diff --git a/plugins/discourse-narrative-bot/config/locales/client.uk.yml b/plugins/discourse-narrative-bot/config/locales/client.uk.yml index 3acfff74e8..7bf1ef3dff 100644 --- a/plugins/discourse-narrative-bot/config/locales/client.uk.yml +++ b/plugins/discourse-narrative-bot/config/locales/client.uk.yml @@ -5,4 +5,9 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -uk: {} +uk: + js: + discourse_narrative_bot: + welcome_post_type: + new_user_track: "Переглянути посібник для нових користувачів" + welcome_message: "Надіслати всім новим користувачам - вітальне повідомлення з швидким переходом до посібника користувача" diff --git a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml index 41c6746496..db42d8e667 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml @@ -7,7 +7,7 @@ tr_TR: site_settings: - discourse_narrative_bot_enabled: 'Botu etkinleştir' + discourse_narrative_bot_enabled: 'Discourse anlatı botunu etkinleştir' badges: certified: name: Sertifikalı diff --git a/plugins/discourse-narrative-bot/config/locales/server.uk.yml b/plugins/discourse-narrative-bot/config/locales/server.uk.yml index 3acfff74e8..c2ab7cfa66 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.uk.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.uk.yml @@ -5,4 +5,25 @@ # To work with us on translations, join this project: # https://www.transifex.com/projects/p/discourse-org/ -uk: {} +uk: + badges: + certified: + name: Сертифікований + discourse_narrative_bot: + dice: + trigger: "крутити" + quote: + '9': + author: "Брюс Лі" + magic_8_ball: + answers: + '9': "Так" + '17': "Моя відповідь - ні" + track_selector: + reset_trigger: 'розпочати' + skip_trigger: 'пропустити' + help_trigger: 'показати допомогу' + new_user_narrative: + reset_trigger: "новий користувач" + advanced_user_narrative: + reset_trigger: 'професійний користувач' From 8d14d55fc5e548c7f949663b3dffe2674e3a298a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 6 Oct 2017 16:48:11 +0200 Subject: [PATCH 107/108] make rubocop happy --- app/models/topic_user.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index a66b6c41af..a141de281d 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -40,13 +40,13 @@ class TopicUser < ActiveRecord::Base def auto_notification(user_id, topic_id, reason, notification_level) should_change = TopicUser .where(user_id: user_id, topic_id: topic_id) - .where("notifications_reason_id IS NULL OR (notification_level < :min AND notification_level > :max)", min: notification_level, max: notification_levels[:regular]) + .where("notifications_reason_id IS NULL OR (notification_level < :min AND notification_level > :max)", min: notification_level, max: notification_levels[:regular]) .exists? change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) if should_change end - def auto_notification_for_staging(user_id, topic_id, reason, notification_level=notification_levels[:watching]) + def auto_notification_for_staging(user_id, topic_id, reason, notification_level = notification_levels[:watching]) change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) end From 7ed522c8909c41e76ef743d6f06c033630030659 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 6 Oct 2017 11:28:49 -0400 Subject: [PATCH 108/108] Version bump to v1.9.0.beta12 --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index 1db5d75b2a..6d524226ed 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 1 MINOR = 9 TINY = 0 - PRE = 'beta11' + PRE = 'beta12' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end