From e1f7d056772f3f1e7312d1e87e3aa5928c3dbf94 Mon Sep 17 00:00:00 2001 From: Ryan Mulligan Date: Wed, 2 Mar 2016 15:11:33 -0800 Subject: [PATCH 001/123] phpbb-import-script: move bbcode_to_md to before other text processing This seems to fix the issue I reported at https://meta.discourse.org/t/import-script-phpbb/40424 --- script/import_scripts/phpbb3/importer.rb | 4 +++- .../import_scripts/phpbb3/support/text_processor.rb | 13 +++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/script/import_scripts/phpbb3/importer.rb b/script/import_scripts/phpbb3/importer.rb index 9f88a73b2b..83d748fa6e 100644 --- a/script/import_scripts/phpbb3/importer.rb +++ b/script/import_scripts/phpbb3/importer.rb @@ -172,8 +172,10 @@ module ImportScripts::PhpBB3 # no need for this since the importer sets last_seen_at for each user during the import end + # Do not use the bbcode_to_md in base.rb. If enabled, it will be + # used in text_processor.rb instead. def use_bbcode_to_md? - @settings.use_bbcode_to_md + false end def batches diff --git a/script/import_scripts/phpbb3/support/text_processor.rb b/script/import_scripts/phpbb3/support/text_processor.rb index c0e99e4dd2..edd104f6d3 100644 --- a/script/import_scripts/phpbb3/support/text_processor.rb +++ b/script/import_scripts/phpbb3/support/text_processor.rb @@ -9,6 +9,7 @@ module ImportScripts::PhpBB3 @database = database @smiley_processor = smiley_processor + @settings = settings @new_site_prefix = settings.new_site_prefix create_internal_link_regexps(settings.original_site_prefix) end @@ -18,6 +19,9 @@ module ImportScripts::PhpBB3 text = CGI.unescapeHTML(text) clean_bbcodes(text) + if @settings.use_bbcode_to_md + text = bbcode_to_md(text) + end process_smilies(text) process_links(text) process_lists(text) @@ -46,6 +50,15 @@ module ImportScripts::PhpBB3 text.gsub!(/:(?:\w{8})\]/, ']') end + def bbcode_to_md(text) + begin + text.bbcode_to_md(false) + rescue e + puts "Problem converting \n#{text}\n using ruby-bbcode-to-md" + text + end + end + def process_smilies(text) @smiley_processor.replace_smilies(text) end From 54b4fb69db1140f0ae925078ab05bd1d690743fc Mon Sep 17 00:00:00 2001 From: James Kiesel Date: Sat, 5 Mar 2016 10:53:28 +1300 Subject: [PATCH 002/123] FEATURE: Add site setting for disabling mailing list mode site wide --- .../discourse/templates/user/preferences.hbs | 4 +++- app/models/user_option.rb | 5 +++++ config/locales/server.en.yml | 1 + config/site_settings.yml | 3 +++ spec/models/user_option_spec.rb | 22 +++++++++++++++++++ 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index b2fd4443d7..eb80ccd418 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -183,7 +183,9 @@ {{preference-checkbox labelKey="user.email_in_reply_to" checked=model.user_option.email_in_reply_to}} {{preference-checkbox labelKey="user.email_private_messages" checked=model.user_option.email_private_messages}} {{preference-checkbox labelKey="user.email_direct" checked=model.user_option.email_direct}} - {{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}} + {{#unless siteSettings.disable_mailing_list_mode}} + {{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}} + {{/unless}} {{preference-checkbox labelKey="user.email_always" checked=model.user_option.email_always}} {{#unless model.user_option.email_always}}
diff --git a/app/models/user_option.rb b/app/models/user_option.rb index 25ecee9aa6..3a2a0e5364 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -44,6 +44,11 @@ class UserOption < ActiveRecord::Base true end + def mailing_list_mode + return false if SiteSetting.disable_mailing_list_mode + super + end + def update_tracked_topics return unless auto_track_topics_after_msecs_changed? TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 490a94ff9b..2cd7f6e473 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1248,6 +1248,7 @@ en: default_email_private_messages: "Send an email when someone messages the user by default." default_email_direct: "Send an email when someone quotes/replies to/mentions or invites the user by default." default_email_mailing_list_mode: "Send an email for every new post by default." + disable_mailing_list_mode: "Disable sending an email for every new post for all users" default_email_always: "Send an email notification even when the user is active by default." default_email_previous_replies: "Include previous replies in emails by default." diff --git a/config/site_settings.yml b/config/site_settings.yml index 765e6e7484..92ae707135 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1074,6 +1074,9 @@ user_preferences: default_email_private_messages: true default_email_direct: true default_email_mailing_list_mode: false + disable_mailing_list_mode: + default: false + client: true default_email_always: false default_email_previous_replies: enum: 'PreviousRepliesSiteSetting' diff --git a/spec/models/user_option_spec.rb b/spec/models/user_option_spec.rb index e3144989ad..b388cd8d68 100644 --- a/spec/models/user_option_spec.rb +++ b/spec/models/user_option_spec.rb @@ -18,6 +18,28 @@ describe UserOption do end + describe ".mailing_list_mode" do + let!(:forum_user) { Fabricate(:user) } + let!(:mailing_list_user) { Fabricate(:user) } + + before do + forum_user.user_option.update(mailing_list_mode: false) + mailing_list_user.user_option.update(mailing_list_mode: true) + end + + it "should return false when `SiteSetting.disable_mailing_list_mode` is enabled" do + SiteSetting.expects(:disable_mailing_list_mode).twice.returns(true) + expect(forum_user.user_option.mailing_list_mode).to eq(false) + expect(mailing_list_user.user_option.mailing_list_mode).to eq(false) + end + + it "should return the stored value when `SiteSetting.disable_mailing_list_mode` is disabled" do + SiteSetting.expects(:disable_mailing_list_mode).twice.returns(false) + expect(forum_user.user_option.mailing_list_mode).to eq(false) + expect(mailing_list_user.user_option.mailing_list_mode).to eq(true) + end + end + describe ".redirected_to_top" do let!(:user) { Fabricate(:user) } From a225c0fbbbd531727f6f62d0b2158a31dd4c32a6 Mon Sep 17 00:00:00 2001 From: joao Date: Sun, 6 Mar 2016 21:06:40 +0000 Subject: [PATCH 003/123] Force users to select an option in required custom user dropdown fields --- .../javascripts/discourse/components/user-field.js.es6 | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/components/user-field.js.es6 b/app/assets/javascripts/discourse/components/user-field.js.es6 index 86caf3b5d6..b4e94e9a80 100644 --- a/app/assets/javascripts/discourse/components/user-field.js.es6 +++ b/app/assets/javascripts/discourse/components/user-field.js.es6 @@ -5,8 +5,6 @@ export default Ember.Component.extend({ layoutName: fmt('field.field_type', 'components/user-fields/%@'), noneLabel: function() { - if (!this.get('field.required')) { - return 'user_fields.none'; - } - }.property('field.required') + return 'user_fields.none'; + }.property() }); From 622d804d4652fc6580c42a99dcbd8a9fdf03ad3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 7 Mar 2016 16:56:17 +0100 Subject: [PATCH 004/123] FEATURE: Add rejection message on rejected IncomingEmail FIX: Better RateLimit description in rejected IncomingEmail FEATURE: Send email when hitting a rate limit --- .../templates/modal/admin_incoming_email.hbs | 12 ++++++++++++ app/controllers/admin/email_controller.rb | 2 +- app/jobs/scheduled/poll_mailbox.rb | 16 ++++++++++++++-- .../incoming_email_details_serializer.rb | 1 + config/locales/client.en.yml | 1 + config/locales/server.en.yml | 11 +++++++++++ ...17_add_rejection_message_to_incoming_email.rb | 5 +++++ lib/email/receiver.rb | 16 +++++++++------- lib/rate_limiter/limit_exceeded.rb | 3 +-- spec/components/email/receiver_spec.rb | 2 +- 10 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 db/migrate/20160303234317_add_rejection_message_to_incoming_email.rb diff --git a/app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs b/app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs index d4907a7e32..6ba2b006f3 100644 --- a/app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs +++ b/app/assets/javascripts/admin/templates/modal/admin_incoming_email.hbs @@ -96,3 +96,15 @@
+{{#if model.rejection_message}} + +
+ +
+ +
+ {{textarea value=model.rejection_message}} +
+
+ +{{/if}} diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 8ca208afb8..33e8c376d1 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -47,7 +47,7 @@ class Admin::EmailController < Admin::AdminController def handle_mail params.require(:email) - Email::Receiver.new(params[:email]).process + Email::Receiver.new(params[:email]).process! render text: "email was processed" end diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index 01ac885b44..26b8453eb5 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -23,9 +23,14 @@ module Jobs def process_popmail(popmail) begin mail_string = popmail.pop - Email::Receiver.new(mail_string).process + receiver = Email::Receiver.new(mail_string) + receiver.process! rescue => e - handle_failure(mail_string, e) + rejection_message = handle_failure(mail_string, e) + if rejection_message.present? && receiver && receiver.incoming_email + receiver.incoming_email.rejection_message = rejection_message.body.to_s + receiver.incoming_email.save + end end end @@ -49,6 +54,7 @@ module Jobs 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 + when RateLimiter::LimitExceeded then :email_reject_rate_limit_specified end template_args = {} @@ -59,6 +65,10 @@ module Jobs template_args[:post_error] = e.message end + if message_template == :email_reject_rate_limit_specified + template_args[:rate_limit_description] = e.description + end + if message_template # inform the user about the rejection message = Mail::Message.new(mail_string) @@ -68,6 +78,8 @@ module Jobs client_message = RejectionMailer.send_rejection(message_template, message.from, template_args) Email::Sender.new(client_message, message_template).send + + client_message else Discourse.handle_job_exception(e, error_context(@args, "Unrecognized error type when processing incoming email", mail: mail_string)) end diff --git a/app/serializers/incoming_email_details_serializer.rb b/app/serializers/incoming_email_details_serializer.rb index ac9e346caa..0d8b834779 100644 --- a/app/serializers/incoming_email_details_serializer.rb +++ b/app/serializers/incoming_email_details_serializer.rb @@ -2,6 +2,7 @@ class IncomingEmailDetailsSerializer < ApplicationSerializer attributes :error, :error_description, + :rejection_message, :return_path, :date, :from, diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index af4246d77d..065c7e9a0c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2296,6 +2296,7 @@ en: cc: "Cc" subject: "Subject" body: "Body" + rejection_message: "Rejection Mail" filters: from_placeholder: "from@example.com" to_placeholder: "to@example.com" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 35f537c9bb..7811d97b0e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -406,11 +406,15 @@ en: first_day_topics_per_day: "You've reached the maximum number of topics a new user can create on their first day. Please wait %{time_left} before trying again." create_topic: "You're creating topics too quickly. Please wait %{time_left} before trying again." create_post: "You're replying too quickly. Please wait %{time_left} before trying again." + delete_post: "You're deleting posts too quickly. Please wait %{time_left} before trying again." topics_per_day: "You've reached the maximum number of new topics today. Please wait %{time_left} before trying again." pms_per_day: "You've reached the maximum number of messages today. Please wait %{time_left} before trying again." create_like: "You've reached the maximum number of likes today. Please wait %{time_left} before trying again." create_bookmark: "You've reached the maximum number of bookmarks today. Please wait %{time_left} before trying again." edit_post: "You've reached the maximun number of edits today. Please wait %{time_left} before trying again." + live_post_counts: "You've asking for live post counts too quickly. Please wait %{time_left} before trying again." + unsubscribe_via_email: "You've reached the maximum number of unsubscribe via email today. Please wait %{time_left} before trying again." + topic_invitations_per_day: "You've reached the maximum number of topic invitations today. Please wait %{time_left} before trying again." hours: one: "1 hour" @@ -1885,6 +1889,13 @@ en: If you can correct the problem, please try again. + email_reject_rate_limit_specified: + subject_template: "[%{site_name}] Email issue -- Rate limited" + text_body_template: | + We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. + + Reason: %{rate_limit_description} + email_reject_invalid_post_action: subject_template: "[%{site_name}] Email issue -- Invalid Post Action" text_body_template: | diff --git a/db/migrate/20160303234317_add_rejection_message_to_incoming_email.rb b/db/migrate/20160303234317_add_rejection_message_to_incoming_email.rb new file mode 100644 index 0000000000..da873df403 --- /dev/null +++ b/db/migrate/20160303234317_add_rejection_message_to_incoming_email.rb @@ -0,0 +1,5 @@ +class AddRejectionMessageToIncomingEmail < ActiveRecord::Migration + def change + add_column :incoming_emails, :rejection_message, :text + end +end diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index fa7f81b912..442fab9fad 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -23,6 +23,8 @@ module Email class InvalidPost < ProcessingError; end class InvalidPostAction < ProcessingError; end + attr_reader :incoming_email + def initialize(mail_string) raise EmptyEmailError if mail_string.blank? @raw_email = mail_string @@ -30,7 +32,7 @@ module Email raise NoMessageIdError if @mail.message_id.blank? end - def process + def process! @from_email, @from_display_name = parse_from_field @incoming_email = find_or_create_incoming_email process_internal @@ -40,12 +42,12 @@ module Email end def find_or_create_incoming_email - IncomingEmail.find_or_create_by(message_id: @mail.message_id) do |incoming_email| - incoming_email.raw = @raw_email - incoming_email.subject = subject - incoming_email.from_address = @from_email - incoming_email.to_addresses = @mail.to.map(&:downcase).join(";") if @mail.to.present? - incoming_email.cc_addresses = @mail.cc.map(&:downcase).join(";") if @mail.cc.present? + IncomingEmail.find_or_create_by(message_id: @mail.message_id) do |ie| + ie.raw = @raw_email + ie.subject = subject + ie.from_address = @from_email + ie.to_addresses = @mail.to.map(&:downcase).join(";") if @mail.to.present? + ie.cc_addresses = @mail.cc.map(&:downcase).join(";") if @mail.cc.present? end end diff --git a/lib/rate_limiter/limit_exceeded.rb b/lib/rate_limiter/limit_exceeded.rb index ad7a000577..b4a49e2918 100644 --- a/lib/rate_limiter/limit_exceeded.rb +++ b/lib/rate_limiter/limit_exceeded.rb @@ -9,7 +9,6 @@ class RateLimiter end def description - time_left = "" if @available_in < 1.minute.to_i time_left = I18n.t("rate_limiter.seconds", count: @available_in) @@ -20,7 +19,7 @@ class RateLimiter end if @type.present? - type_key = @type.gsub(/-/, '_') + type_key = @type.tr("-", "_") msg = I18n.t("rate_limiter.by_type.#{type_key}", time_left: time_left, default: "") return msg if msg.present? end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 734276c6a7..a559555f78 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -13,7 +13,7 @@ describe Email::Receiver do end def process(email_name) - Email::Receiver.new(email(email_name)).process + Email::Receiver.new(email(email_name)).process! end it "raises an EmptyEmailError when 'mail_string' is blank" do From 186a9630b7c0520b450eff3c516023c6ab8a2427 Mon Sep 17 00:00:00 2001 From: Joe Buhlig Date: Fri, 4 Mar 2016 14:41:59 -0600 Subject: [PATCH 005/123] Added spec for topic status update event --- spec/models/topic_spec.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 3799738ad1..6d0e7074e2 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1584,4 +1584,20 @@ describe Topic do expect(topic.message_archived?(user)).to eq(true) end + + it 'will trigger :topic_status_updated' do + topic = Fabricate(:topic) + user = topic.user + user.admin = true + @topic_status_event_triggered = false + + DiscourseEvent.on(:topic_status_updated) do + @topic_status_event_triggered = true + end + + topic.update_status('closed', true, user) + topic.reload + + expect(@topic_status_event_triggered).to eq(true) + end end From b716886240d044b1770e0657e5e5894254bbfeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 7 Mar 2016 18:21:09 +0100 Subject: [PATCH 006/123] update bbPress importer --- script/import_scripts/base.rb | 10 +- script/import_scripts/bbpress.rb | 235 ++++++++++++++++++++----------- 2 files changed, 157 insertions(+), 88 deletions(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index 63c55de590..5c4cd5bc80 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -197,20 +197,20 @@ class ImportScripts::Base def all_records_exist?(type, import_ids) return false if import_ids.empty? - Post.exec_sql('create temp table import_ids(val varchar(200) primary key)') + Post.exec_sql('CREATE TEMP TABLE import_ids(val varchar(200) PRIMARY KEY)') - import_id_clause = import_ids.map{|id| "('#{PG::Connection.escape_string(id.to_s)}')"}.join(",") - Post.exec_sql("insert into import_ids values #{import_id_clause}") + import_id_clause = import_ids.map { |id| "('#{PG::Connection.escape_string(id.to_s)}')" }.join(",") + Post.exec_sql("INSERT INTO import_ids VALUES #{import_id_clause}") existing = "#{type.to_s.classify}CustomField".constantize.where(name: 'import_id') - existing = existing.joins('JOIN import_ids ON val=value') + existing = existing.joins('JOIN import_ids ON val = value') if existing.count == import_ids.length puts "Skipping #{import_ids.length} already imported #{type}" return true end ensure - Post.exec_sql('drop table import_ids') + Post.exec_sql('DROP TABLE import_ids') end # Iterate through a list of user records to be imported. diff --git a/script/import_scripts/bbpress.rb b/script/import_scripts/bbpress.rb index aba552d11e..87ee57bdd1 100644 --- a/script/import_scripts/bbpress.rb +++ b/script/import_scripts/bbpress.rb @@ -1,124 +1,193 @@ -# `dropdb bbpress` -# `createdb bbpress` -# `bundle exec rake db:migrate` - require 'mysql2' require File.expand_path(File.dirname(__FILE__) + "/base.rb") -BB_PRESS_DB = ENV['BBPRESS_DB'] || "bbpress" -DB_TABLE_PREFIX = "wp_" - class ImportScripts::Bbpress < ImportScripts::Base + BB_PRESS_DB ||= ENV['BBPRESS_DB'] || "bbpress" + BATCH_SIZE ||= 1000 + def initialize super @client = Mysql2::Client.new( host: "localhost", username: "root", - #password: "password", - database: BB_PRESS_DB + database: BB_PRESS_DB, ) end - def table_name(name) - DB_TABLE_PREFIX + name - end - def execute - users_results = @client.query(" - SELECT id, - user_login username, - display_name name, - user_url website, - user_email email, - user_registered created_at - FROM #{table_name 'users'}", cache_rows: false) - - puts '', "creating users" - - create_users(users_results) do |u| - ActiveSupport::HashWithIndifferentAccess.new(u) - end - - - puts '', '', "creating categories" - - create_categories(@client.query("SELECT id, post_name, post_parent from #{table_name 'posts'} WHERE post_type = 'forum' AND post_name != '' ORDER BY post_parent")) do |c| - result = {id: c['id'], name: c['post_name']} - parent_id = c['post_parent'].to_i - if parent_id > 0 - result[:parent_category_id] = category_id_from_imported_category_id(parent_id) - end - result - end - - import_posts + import_users + import_categories + import_topics_and_posts end - def import_posts - puts '', "creating topics and posts" + def import_users + puts "", "importing users..." - total_count = @client.query(" - SELECT count(*) count - FROM #{table_name 'posts'} + last_user_id = -1 + total_users = bbpress_query("SELECT COUNT(*) count FROM wp_users WHERE user_email LIKE '%@%'").first["count"] + + batches(BATCH_SIZE) do |offset| + users = bbpress_query(<<-SQL + SELECT id, user_nicename, display_name, user_email, user_registered, user_url + FROM wp_users + WHERE user_email LIKE '%@%' + AND id > #{last_user_id} + ORDER BY id + LIMIT #{BATCH_SIZE} + SQL + ).to_a + + break if users.empty? + + last_user_id = users[-1]["id"] + user_ids = users.map { |u| u["id"].to_i } + + next if all_records_exist?(:users, user_ids) + + user_ids_sql = user_ids.join(",") + + users_description = {} + bbpress_query(<<-SQL + SELECT user_id, meta_value description + FROM wp_usermeta + WHERE user_id IN (#{user_ids_sql}) + AND meta_key = 'description' + SQL + ).each { |um| users_description[um["user_id"]] = um["description"] } + + users_last_activity = {} + bbpress_query(<<-SQL + SELECT user_id, meta_value last_activity + FROM wp_usermeta + WHERE user_id IN (#{user_ids_sql}) + AND meta_key = 'last_activity' + SQL + ).each { |um| users_last_activity[um["user_id"]] = um["last_activity"] } + + create_users(users, total: total_users, offset: offset) do |u| + { + id: u["id"].to_i, + username: u["user_nicename"], + email: u["user_email"].downcase, + name: u["display_name"], + created_at: u["user_registered"], + website: u["user_url"], + bio_raw: users_description[u["id"]], + last_seen_at: users_last_activity[u["id"]], + } + end + end + end + + def import_categories + puts "", "importing categories..." + + categories = bbpress_query(<<-SQL + SELECT id, post_name, post_parent + FROM wp_posts + WHERE post_type = 'forum' + AND LENGTH(COALESCE(post_name, '')) > 0 + ORDER BY post_parent, id + SQL + ) + + create_categories(categories) do |c| + category = { id: c['id'], name: c['post_name'] } + if (parent_id = c['post_parent'].to_i) > 0 + category[:parent_category_id] = category_id_from_imported_category_id(parent_id) + end + category + end + end + + def import_topics_and_posts + puts "", "importing topics and posts..." + + last_post_id = -1 + total_posts = bbpress_query(<<-SQL + SELECT COUNT(*) count + FROM wp_posts WHERE post_status <> 'spam' - AND post_type IN ('topic', 'reply')").first['count'] + AND post_type IN ('topic', 'reply') + SQL + ).first["count"] - batch_size = 1000 + batches(BATCH_SIZE) do |offset| + posts = bbpress_query(<<-SQL + SELECT id, + post_author, + post_date, + post_content, + post_title, + post_type, + post_parent + FROM wp_posts + WHERE post_status <> 'spam' + AND post_type IN ('topic', 'reply') + AND id > #{last_post_id} + ORDER BY id + LIMIT #{BATCH_SIZE} + SQL + ).to_a - batches(batch_size) do |offset| - results = @client.query(" - SELECT id, - post_author, - post_date, - post_content, - post_title, - post_type, - post_parent - FROM #{table_name 'posts'} - WHERE post_status <> 'spam' - AND post_type IN ('topic', 'reply') - ORDER BY id - LIMIT #{batch_size} - OFFSET #{offset}", cache_rows: false) + break if posts.empty? - break if results.size < 1 + last_post_id = posts[-1]["id"].to_i + post_ids = posts.map { |p| p["id"].to_i } - next if all_records_exist? :posts, results.map {|p| p["id"].to_i} + next if all_records_exist?(:posts, post_ids) - create_posts(results, total: total_count, offset: offset) do |post| + post_ids_sql = post_ids.join(",") + + posts_likes = {} + bbpress_query(<<-SQL + SELECT post_id, meta_value likes + FROM wp_postmeta + WHERE post_id IN (#{post_ids_sql}) + AND meta_key = 'Likes' + SQL + ).each { |pm| posts_likes[pm["post_id"]] = pm["likes"].to_i } + + create_posts(posts, total: total_posts, offset: offset) do |p| skip = false - mapped = {} - mapped[:id] = post["id"] - mapped[:user_id] = user_id_from_imported_user_id(post["post_author"]) || find_user_by_import_id(post["post_author"]).try(:id) || -1 - mapped[:raw] = post["post_content"] - if mapped[:raw] - mapped[:raw] = mapped[:raw].gsub("
", "```\n").gsub("
", "\n```") + post = { + id: p["id"], + user_id: user_id_from_imported_user_id(p["post_author"]) || find_user_by_import_id(p["post_author"]).try(:id) || -1, + raw: p["post_content"], + created_at: p["post_date"], + like_count: posts_likes[p["id"]], + } + + if post[:raw].present? + post[:raw].gsub!("
", "```\n")
+          post[:raw].gsub!("
", "\n```") end - mapped[:created_at] = post["post_date"] - mapped[:custom_fields] = {import_id: post["id"]} - if post["post_type"] == "topic" - mapped[:category] = category_id_from_imported_category_id(post["post_parent"]) - mapped[:title] = CGI.unescapeHTML post["post_title"] + if p["post_type"] == "topic" + post[:category] = category_id_from_imported_category_id(p["post_parent"]) + post[:title] = CGI.unescapeHTML(p["post_title"]) else - parent = topic_lookup_from_imported_post_id(post["post_parent"]) - if parent - mapped[:topic_id] = parent[:topic_id] - mapped[:reply_to_post_number] = parent[:post_number] if parent[:post_number] > 1 + if parent = topic_lookup_from_imported_post_id(p["post_parent"]) + post[:topic_id] = parent[:topic_id] + post[:reply_to_post_number] = parent[:post_number] if parent[:post_number] > 1 else - puts "Skipping #{post["id"]}: #{post["post_content"][0..40]}" + puts "Skipping #{p["id"]}: #{p["post_content"][0..40]}" skip = true end end - skip ? nil : mapped + skip ? nil : post end end end + def bbpress_query(sql) + @client.query(sql, cache_rows: false) + end + end ImportScripts::Bbpress.new.perform From 6e3dcdeea17c9a58745eb8082d16c90782f5afcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 7 Mar 2016 19:17:14 +0100 Subject: [PATCH 007/123] update email_reply_trimmer to latest --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 832d95315b..93455abd85 100644 --- a/Gemfile +++ b/Gemfile @@ -66,7 +66,7 @@ gem 'aws-sdk', require: false gem 'excon', require: false gem 'unf', require: false -gem 'email_reply_trimmer', '0.0.8' +gem 'email_reply_trimmer', '0.1.1' # note: for image_optim to correctly work you need to follow # https://github.com/toy/image_optim diff --git a/Gemfile.lock b/Gemfile.lock index 875f984536..9b56f0feb6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,7 +76,7 @@ GEM docile (1.1.5) domain_name (0.5.25) unf (>= 0.0.5, < 1.0.0) - email_reply_trimmer (0.0.8) + email_reply_trimmer (0.1.1) ember-data-source (1.0.0.beta.16.1) ember-source (~> 1.8) ember-handlebars-template (0.1.5) @@ -414,7 +414,7 @@ DEPENDENCIES byebug certified discourse-qunit-rails - email_reply_trimmer (= 0.0.8) + email_reply_trimmer (= 0.1.1) ember-rails ember-source (= 1.12.2) excon From b49e0e0f4a8480e49e532ba75e4a6395043ec203 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 7 Mar 2016 13:40:14 -0500 Subject: [PATCH 008/123] FIX: add path to cookie on subfolder installs --- config/initializers/100-session_store.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/initializers/100-session_store.rb b/config/initializers/100-session_store.rb index 2a14105c92..e89e764e86 100644 --- a/config/initializers/100-session_store.rb +++ b/config/initializers/100-session_store.rb @@ -1,6 +1,10 @@ # Be sure to restart your server when you modify this file. -Discourse::Application.config.session_store :cookie_store, key: '_forum_session' +Discourse::Application.config.session_store( + :cookie_store, + key: '_forum_session', + path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root +) # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information From d7bc3403102b92e8f88dfc9aec428a9f3ed3fd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 7 Mar 2016 19:47:40 +0100 Subject: [PATCH 009/123] FIX: Emoji.clear_cached wasn't deleting the right caches --- app/models/emoji.rb | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/app/models/emoji.rb b/app/models/emoji.rb index e636ae1686..10755173b7 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -1,4 +1,7 @@ class Emoji + # update this to clear the cache + EMOJI_VERSION = "v2" + include ActiveModel::SerializerSupport attr_reader :path @@ -20,19 +23,19 @@ class Emoji end def self.all - Discourse.cache.fetch("all_emojis:v2") { standard | custom } + Discourse.cache.fetch("all_emojis:#{EMOJI_VERSION}") { standard | custom } end def self.standard - Discourse.cache.fetch("standard_emojis:v2") { load_standard } + Discourse.cache.fetch("standard_emojis:#{EMOJI_VERSION}") { load_standard } end def self.aliases - Discourse.cache.fetch("aliases_emojis:v2") { load_aliases } + Discourse.cache.fetch("aliases_emojis:#{EMOJI_VERSION}") { load_aliases } end def self.custom - Discourse.cache.fetch("custom_emojis:v2") { load_custom } + Discourse.cache.fetch("custom_emojis:#{EMOJI_VERSION}") { load_custom } end def self.exists?(name) @@ -76,10 +79,10 @@ class Emoji end def self.clear_cache - Discourse.cache.delete("custom_emojis") - Discourse.cache.delete("standard_emojis") - Discourse.cache.delete("aliases_emojis") - Discourse.cache.delete("all_emojis") + Discourse.cache.delete("custom_emojis:#{EMOJI_VERSION}") + Discourse.cache.delete("standard_emojis:#{EMOJI_VERSION}") + Discourse.cache.delete("aliases_emojis:#{EMOJI_VERSION}") + Discourse.cache.delete("all_emojis:#{EMOJI_VERSION}") end def self.db_file From df413737d29ac214fcb563b945555b9b17855e12 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 7 Mar 2016 15:28:02 -0500 Subject: [PATCH 010/123] FIX: render About page for web crawlers --- app/controllers/about_controller.rb | 14 +++- app/views/about/index.html.erb | 100 ++++++++++++++++++++++++++++ app/views/layouts/crawler.html.erb | 2 +- 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 app/views/about/index.html.erb diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index ccd6cdae72..a3957ebda9 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -1,12 +1,22 @@ require_dependency 'rate_limiter' class AboutController < ApplicationController - skip_before_filter :check_xhr, only: [:show] + skip_before_filter :check_xhr, only: [:index] before_filter :ensure_logged_in, only: [:live_post_counts] def index @about = About.new - render_serialized(@about, AboutSerializer) + + respond_to do |format| + format.html do + # @list = list + # store_preloaded(list.preload_key, MultiJson.dump(TopicListSerializer.new(list, scope: guardian))) + render :index + end + format.json do + render_serialized(@about, AboutSerializer) + end + end end def live_post_counts diff --git a/app/views/about/index.html.erb b/app/views/about/index.html.erb new file mode 100644 index 0000000000..d13b4e7861 --- /dev/null +++ b/app/views/about/index.html.erb @@ -0,0 +1,100 @@ +<% content_for :title do %><%=t "about" %><% end %> + +
+

+ <%=t "js.about.title", {title: @about.title} %> +

+ +
+ <%= @about.description %> +
+ +

<%=t "js.about.our_admins" %>

+ + + + <% if @about.moderators.count > 0 %> +

<%=t "js.about.our_moderators" %>

+ + <% end %> + +
+

<%=t 'js.about.stats' %>

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 <%=t 'js.about.stat.all_time' %><%=t 'js.about.stat.last_7_days' %><%=t 'js.about.stat.last_30_days' %>
<%=t 'js.about.topic_count' %><%= @about.stats[:topic_count] %><%= @about.stats[:topics_7_days] %><%= @about.stats[:topics_30_days] %>
<%=t 'js.about.post_count' %><%= @about.stats[:post_count] %><%= @about.stats[:posts_7_days] %><%= @about.stats[:posts_30_days] %>
<%=t 'js.about.user_count' %><%= @about.stats[:user_count] %><%= @about.stats[:users_7_days] %><%= @about.stats[:users_30_days] %>
<%=t 'js.about.active_user_count' %><%= @about.stats[:active_users_7_days] %><%= @about.stats[:active_users_30_days] %>
<%=t 'js.about.like_count' %><%= @about.stats[:like_count] %><%= @about.stats[:likes_7_days] %><%= @about.stats[:likes_30_days] %>
+
+ +
+
+
\ No newline at end of file diff --git a/app/views/layouts/crawler.html.erb b/app/views/layouts/crawler.html.erb index a68cfade2b..732043da7c 100644 --- a/app/views/layouts/crawler.html.erb +++ b/app/views/layouts/crawler.html.erb @@ -19,7 +19,7 @@ <%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %> <%- end %>
- "> + ">
<%= yield %> From 74e4251affeb678b36657a9a83c25248fd2f4e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 7 Mar 2016 21:56:33 +0100 Subject: [PATCH 011/123] FIX: collapse 'replied', 'quoted' and 'posted' to the same notification --- app/services/post_alerter.rb | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 47d9a1b922..a8c6f78a07 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -208,15 +208,19 @@ class PostAlerter end def should_notify_previous?(user, notification, opts) - type = notification.notification_type - if type == Notification.types[:edited] - return should_notify_edit?(notification, opts) - elsif type == Notification.types[:liked] - return should_notify_like?(user, notification) + case notification.notification_type + when Notification.types[:edited] then should_notify_edit?(notification, opts) + when Notification.types[:liked] then should_notify_like?(user, notification) + else false end - return false end + COLLAPSED_NOTIFICATION_TYPES ||= [ + Notification.types[:replied], + Notification.types[:quoted], + Notification.types[:posted], + ] + def create_notification(user, type, post, opts=nil) return if user.blank? return if user.id == Discourse::SYSTEM_USER_ID @@ -268,9 +272,10 @@ class PostAlerter collapsed = false - if type == Notification.types[:replied] || type == Notification.types[:posted] - destroy_notifications(user, Notification.types[:replied], post.topic) - destroy_notifications(user, Notification.types[:posted], post.topic) + if COLLAPSED_NOTIFICATION_TYPES.include?(type) + COLLAPSED_NOTIFICATION_TYPES.each do |t| + destroy_notifications(user, t, post.topic) + end collapsed = true end From 335513de617f4a7c6449cd715dbec5d6eefec3b8 Mon Sep 17 00:00:00 2001 From: sghebuz Date: Mon, 7 Mar 2016 23:15:57 +0100 Subject: [PATCH 012/123] Fix mbox.rb for updated Email::Receiver API This commit https://github.com/discourse/discourse/commit/30836573587079c5e663d7b3122957fc8c70dafe broke mbox importer.it. Update the mbox importer for the new Email::Receiver API --- script/import_scripts/mbox.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/script/import_scripts/mbox.rb b/script/import_scripts/mbox.rb index a7773f0a56..7e93c9e5c6 100755 --- a/script/import_scripts/mbox.rb +++ b/script/import_scripts/mbox.rb @@ -125,11 +125,11 @@ class ImportScripts::Mbox < ImportScripts::Base end def parse_email(msg) - receiver = Email::Receiver.new(msg, skip_sanity_check: true) + receiver = Email::Receiver.new(msg) mail = Mail.read_from_string(msg) mail.body - selected = receiver.select_body(mail) + selected = receiver.select_body selected.force_encoding(selected.encoding).encode("UTF-8") end @@ -147,11 +147,11 @@ class ImportScripts::Mbox < ImportScripts::Base create_posts(topics, total: topic_count, offset: offset) do |t| raw_email = File.read(t['file']) - receiver = Email::Receiver.new(raw_email, skip_sanity_check: true) + receiver = Email::Receiver.new(raw_email) mail = Mail.read_from_string(raw_email) mail.body - selected = receiver.select_body(mail) + selected = receiver.select_body next unless selected raw = selected.force_encoding(selected.encoding).encode("UTF-8") @@ -190,11 +190,11 @@ class ImportScripts::Mbox < ImportScripts::Base next unless topic_id raw_email = File.read(p['file']) - receiver = Email::Receiver.new(raw_email, skip_sanity_check: true) + receiver = Email::Receiver.new(raw_email) mail = Mail.read_from_string(raw_email) mail.body - selected = receiver.select_body(mail) + selected = receiver.select_body raw = selected.force_encoding(selected.encoding).encode("UTF-8") { id: id, From 320d5d64e87cfa0b77f40a51c01db02f34e7516f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20R=C3=BCckert?= Date: Mon, 7 Mar 2016 23:26:28 +0100 Subject: [PATCH 013/123] Use sRGB Color Profile when converting images With the conversion done when uploading or fetching an image we also lose any embedded color profiles. [This leads to images possibly not rendered properly in the browsers.][1] To fix the issue we tell imagemagick to render the image from the embedded color profile to sRGB, which is the color space used by most browsers. RT_sRGB.icm is taken from the [RawTherapee repository][2] and is licensed as Public Domain. [1]: https://meta.discourse.org/t/image-embedded-color-profile/40519 [2]: https://github.com/Beep6581/RawTherapee/blob/master/rtdata/iccprofiles/output/RT_sRGB.icm --- app/models/optimized_image.rb | 2 ++ vendor/data/RT_sRGB.icm | Bin 0 -> 25572 bytes 2 files changed, 2 insertions(+) create mode 100644 vendor/data/RT_sRGB.icm diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index f316a8d2df..590724f21e 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -108,6 +108,7 @@ class OptimizedImage < ActiveRecord::Base -interpolate bicubic -unsharp 2x0.5+0.7+0 -quality 98 + -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')} #{to} } end @@ -130,6 +131,7 @@ class OptimizedImage < ActiveRecord::Base -gravity center -background transparent -resize #{dimensions}#{!!opts[:force_aspect_ratio] ? "\\!" : "\\>"} + -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')} #{to} } end diff --git a/vendor/data/RT_sRGB.icm b/vendor/data/RT_sRGB.icm new file mode 100644 index 0000000000000000000000000000000000000000..3d0822ef7c1bd141a4c933686e47f258201bd7d4 GIT binary patch literal 25572 zcmeI5S5#C@x2|ig&KVk-oO6?N&N*kvIZMt#vVe*pDnXH;B0)q@KtL2EDrQkZK}As% z!Gxj$h7UOH_np1}e~)o4&e;2AUv%{ttKP1jU8~lbRd>&LCjh~3qJv|SU_}68Vv`e{ zEsTXcJiUZ4a{vMqzzX;POJHzPyq$}=D_}HR6FV112mjIkYU_^~(Tp}F_h+tujsJfs zQbuk;3J;8l2^9JxrL4?M)RnZ<)rCS6Qg)^WMu)~G)BpWua*TDl0)LRi=q-RT z5CY+hwZMm;n=@4wQfvSkJ#YX{jQ8fk$odZ84m^M-@B%)-7x*!DK_CbM z!5{>LF?K-&BezkE%*HS>8^_3XB4alsGqRlq(m@8u1X&;(n=J0~6pnxCw57+u#nk2PVON@BmDKY48Zl zfLZVaJOgv!Id}nHf>&Sxya9{gEm#Kczz6UVd;*`r7qAMxf$v})Y=9r&7x)b}!4}wt zAP9z#5DLOTSO^ahAQD7|SRhu23b8{ph!f(5cpyHA9}ur+K8 zJHSq`3+x7az+SK~><uMv+y~%6TSfV!Ts<6d<7nX$KY%54fqy(7oLP4!jIq?_$mAx zo`)CUMR*B*4}XF`!(ZVwcmw_gZ^GLM7=c1y5qJa%!Gd5z&=6b*UW5Qb7$J&~K+qAg z2nB=^LKUHb&_?JX3=zf%GlV6=24RnILbxK_5uOMigg+t(5sHXFL?L1k35aAw8e$hB z8`eWornvFKEx%&AYvFXhL}LyMBG74 zA|4_hA!ZSCh7_<;C~_=;FV{6PFhY$IVL3W-G$kQ5{pNkeiW`H+H05u^l? zj+8|zAXSj+NG+ru(hzBav_M)T?T}7LSEM`A8|jA(LWUwEkkQCEWD+tJxeJ+%%tIC; z_aaM>Wyo@574jIe4tWyUh&+vKL!Lu+A$yVi$U)>VavXUbc?)?D`2hI{`2;zKe2ILG zTta?8t{}f6*N{Jvn<#)nqOd3eih^Q8aiVxo0w@ubIEs#vLn)zDQJN@Slp)FlWr4Cm zIiOrnZYVF5A1VkHhKfYRpb}6is0>s#Dj!vZ+J`Dd9YR&2s!?^Qlc*+CE2-jdWM=uy+JLbKBB&$zN3Di{z3yZ5{*R@(JW|oG#8p5EsPdN z)6sHhCA1n^8?BEvMw_Fp(e`K;v^&}x?T-#YN1&t8@#qwE209y^kKTjck1j)(qmQC% z(I?PN=vH()x(nTlzJwk^kD({fx6za6Y4j|54*d$fh<=Y=L4QMUpnqck28qF8NEj-H z1H+3E#E4;}Fmf0rj5kUtQJ-uYl5}J+F_ls?pPmeAT|seg^kCiV0U42v4z-TY#Fu!TaB&9 zp2D8Swqv`neb@o)2=*HG7IqT*2>TQ}k9~uEhh4#b!~Vc-;Se}1j)bG)IC1#WmnsaP7D*Tpw-_ zH;TKCyMudxo59WD7H~_rPq?qRAGl3C0*}R$@oac*yZ~MdPsc0ZRq@(*L%bQ@2JeV> z!+YZc@!|Mrd?G#_pN%iT7vszDmG~O`34AmDEWQ)phabd`;;-ZH;-~Pl_!sy$`1klP z_%-}*0z|+NNCYZ@iy%M{BS;ez32Fo#f)T-jU`KEvcoO^xp@b+x0wI-` z%fuDp8u2#?CSgfr5{<-55+O;E6i8|$9g;E0lH@?zLGmF5k#>^eNU5YOQUPf{=@98C zsh-qCY9n=#`bk5iYoyzxhomQ@dD2_bC(<|4FET{NkjZ2knU^d=rjr%P>SR5#DcOeX zMD`&2lf%d{v@lQ2Z%jlo(1fC6lt7vY&F8QcXEQX`ysbdME>wamp>q z1IjFAp7NHmLRq8y#e!fVuuxgJS%g@mSQJ>)S@c-USZrBbSiD(+Saz}`u%xr(vg~Ck zW2s`PXK7+-XSu*~nPrUSCd&hsS(bU0C6*PIb(T$56f2RHot2kWlvSElnN^F`h}DYK zk=29MpEaB{mNk_%hqZ|H0Ba>{9cvToS=I}z1FU1Lw^$#tK4E>u`i^y#^#>JDu~Zf+ zCsmLtNmZb#Q}wClRC}r$)t4GdjiIJcv#CYY1Jp`t9kq$tPVJ!%Qm<0)P^YPL)Hl?R z)bG^4*pO^QHg+~XHZe9?HdQuVHd8iRHdi(uwh*>xwq&*}wnDa2wko!Iwq~{twqCX? zY!htv*dDXJV0+89!nVP-&5mKGuye5su}iTlvTLy$v0Je_vwN`zu}85dv1hUuvX`<~ zve&aWvv;ufu@A9dXP;!BVSmZK%)ZM0lLpi9G&UM9O^hZ>Q={q8%xU&CcbY#ff)-E9 zpzWsZr&Z8uX-%{aS}$#gHbI-D&C*`d-qF6&esdr=h#WKyehvu^1rALPLk=qrXAW*mCv=GtAeYZtA*=4*CnnouG?IXxL$BAajkOw;zn?jxH-6m zxar)=+`8Om+z#9x+(Fz?+{xTI+P`1P_Ua zlShb0nn#64kH>b!0WXf1 zjhCNSl2?gWo7a@rp4Wpnh&P%yl{b%fKW_zZJ@0AWF5Us&YrK=ZPk3MRe&XHWgZK!1 zG(JH-I-d%k9-jrD6Q2)X7+*Z!F1|v(GQMiQQ+yqK7x_l{Zu33jo9BDa_nmKxAIneW z=jWH?SK`;!bkK@naFXS)dujX&$@8G}4KgNHD|1tk7{tx`?0zd#SKobxY zkQPuCFc7d3a24j#HC&Vr!AVe2Z6*3UA z7TO^cAQUB(B9tdoB6LKkL8x8mqR^PoU7=Z_*Fq~oKZTLP6k#4=abZPa9bt1}Ct+XV z2;n5*9N}W&O5qd2ZNk06qr!KDXM`7oKMDU7L5fgBctpfSltgqzEJR#H{6uz&B#Y#U zl!zP=IVI8|(l2sVWK!g*$Xk(BkxfyoD4VFDsI;iMsFA3hsE25XXq;%K=pNB>(R$G{ zqCKJ`qPIn7L>EL?M1P8*#8}1n#H7Sj#SFx3#N5P!#A3xV#EQfYi`9#@iuH(%h}{;O z5qmB6S?sqsTAV5_ATBMgCT=8dC+;a8DxM&oExu2@Qv9TNyLi9&Rq^}cbK=Y5-z9(q zQG!cCOhQpYSHe=lRU$wlS|VMdP~wn8okXidkHm&>b=xD9YLqi`RGz~HM$Yqp6*2trzg?#=m+S>=q>aM^kMoP z`Ye5s{*}HZO_1i27ME6%)|a-Cc9#y3PLR%#E|ESe-6Y*5JtTcwdPe$<^s4lh3_*rV zModOYMqkE8#zQ7lCQ&9=rc~ybOpD9~nGu=0GEZfeWWLKnvSe9aSt(gHSz}oTSs&S* zvZ=C#vWI1l%bu0(m%S$YQ1+$lC)r*-gW#zTxE#zJ0gXH7nv*q{8SIalccgv5+-<5wR|4x2g0ja>M zAgCa#psiq`utOnOAzmRzp;Y0R!fAybg)xQu3eOcjDEw5!D6%VxC@L!IE7~Y}Duyej zDDGA~q4JA`0XQcq8Sfwnb{YuqJElNE~V@mgx zUMPK3`lXCj=1>+>R#rAtc2M?Jj#AE0-m6@t+^F28JfeJ0c~1Gg@(&e^3Qa{+MM=d# z#a_inB~m3_Wv@z=N~21*%81H6mFFrSRDP;rRXJ3}RaI1tR2^0QRAW>#Rf|=tRa;bh zRmW8ys=ieHth%X2RO3;jt7)p4tL;z=R!daNS39V7T&-PgK<$>=tlE;=x;j#wOt<*(PBSSpEi-E~Z?kB#Y_l@66K0)eV`fukZ_L)r(dL}yQs!Fb z*5=;kG3Ghu2hC5KcbSiwPn$2AuUlX&xGbbCbS!Kw{4C-u@+=NpG+OjnT(g+5Sho0S zNwDOzl(#gnbhHezOtLJpJYv~udCBs&<(%aw%WW%GD-kPID>Ey1s|c%IRwY*TR_Coo ztR7muwpz2sSaVrRTkBfeS^HZjSQl7VTDMsDTi>#NX8p-}+lJLf#75P|+{VKu(k9F1 zfXxY;E}Lb0~KWz!N{I&|VMz$`tp|)wZ#kRG!9k#=^4{TrCuGwMixb0-@^z0n$ z0_~FQitMWG+Uy4H?%BPx`)ZG}=d`EW>)6}d2iPat7up}SKVv^&f7kw{{Z|L11E&Ms zLD#|FA;2NYp~&H=Lz}~(!=%G2hwqMPM{Y+MM}0>}$6&`4$9;}9jvbD}jt?Cd9XFit zPJB)ZPR35IPT@|woJyTeICVQ+b((Q{@3iSmaTak_bGCH$a*lD%bFOf1aqf4%?fk-d z)dlIo=_2i->*C-N+wxoflQCD%Ky z^R8capmuQWklCTX!+A&Oj`STRJC5(@-f?Zm?2ZpRw%w?1;%=I5wr>7ziEew`j=8nF zjkry@ExG-2C%Fr|tGZjdd%MTF?{=?pKjS{=e&7AI`-TVJL%>7X!_33eBibX+qr&5~ z$AHH@j|Go)Pn;*er;?|sr-x^>XRc?3=V{LY&wHK=p6gzCFMcm2FEcMsuNbd0|2?;FIjL&!^6( z)90$s6Q57MkT1=b?yKkP>>K90%eTz;lTlV|QpVeR7U(4U#Kgd7Lzr_Efe~EechL2q zxuDfxbTCh_La=GDXK-wAL2z|&NAOtiOz_7LD1;+KCd4qrEhH)=FQh7@Eo3-kI^V9JFGmcHS9{*RM>LZW;k28 zRJeY)OL#F{^q+Y#&$(h-IcJ0hYY@*<8zoQ)Wbn2Gqf6W+^#5o+RkS?S0mApe38nLmXUsuNs+~oCn9?zZ%4k0+=wDZi9~5e*++#$ z?TR`S)e7KkZmrXWI3&`Ly+Pa=LiBZn|rFRC+;rO?r3w&Gc94KQdS{ zBr^;$+%jS_iZbdmdNb~1yvg{zi*1+8F5_KZyApR5?`qg}Y1jQ-%b6gPD^nrUBGW%J zEwe1MC37hAapp=EI!ho+Ez2$|G%Gu+Dyt*wTGsQdwQN$hc(z{lj_jE1qU`$Y-t4>C zi`knwv>dq{vmD=?l$^4hmYkuS$2lvx=v={Eja-M^@Z8+o>fFxU8@VrYf9A2~(esS+ zyz&zBO7a@>2J)u!KIWtH`SaED?eoL(bMuepcjn*7f0h4pH+8r4Zj;^KyOVbx*xkH) zX!qmYp9`=BLIqj{&IOSL1qF2lJq33Q-WF^XauzBUS``Kr?kcP(Y%jc4_@Z#Vh*Bg~ zWK`r;lvGq&)Le9>=yB2KJ=i_Md$jks?1|n}wCDJqi+k?xdAAqd%ePl;ul?Tey?J|& z?d{%sd+(dQTl+ZoDekk{7ql;HU)8>I`>yYMweMFkTd{1hd9inWxo!x zACy06c`)c;=E16i=MUaI`1;`HAD>*Bb zD{U*oD)TFAEBh)RRDP&JR|!?=RPCsWuPUi(t{SO&TD5kB<%skVvm*gVb{(lY(s|_e zk;S9nDDP4AqmDG?ky^gg` zw$7q1xGtyeSY2=3{kjkJn0k?V{d&*(-0^ty@x8|zj}IMx zdVK8!>j~KtmM20^f0{2&9r?#%W_ukto7OOvxR3*p1pGR>Di5T_I9Oq`}WB8z3oly zqwO!+e|K<3Fc;&i!1~!gb-nHe-2&Y@-Jad4 z-Id*4-ILuPFW@goUNE~5d?D|`@e2bNo?O`IVee7yaqNldDd{=WGtu*=7w#46)$8@{ z&FDSa+td57_e&qSPqxpxFQTugud#2mZ@zEyBJV}5i|!XwE>>LZx;T09Q$L}f-fz(# z+F#J$&_CS&y#Mzlo=cjS+%6?wD!F7-U00a zkAc*Is(}jw4+p*sk_Y7mZ3ZI;_YIyNyf*me3VcQQioq4XD_K`+uUxt^du4ryHl#Y_ zJd`kWaOm97-JuV|_+k36<#5<=(Qwo7`0%R{Fd{gjKjJfzIZ`unX=HX}W0Yf5b<|}v zarE$L=ji0<${1-(cFblha;$i)b?o}s+i}#m*tp4f(0KlM!}!Sf{P@;Y{;RrIeXeF+ zt+{&X>g?4Y*Ep}KU)yml$2Bvu18(p zf4%Mc&Fk-OU~fp>u(%O^W6zD28xuDcZ=!CB-88)!aVZP z)o^R{*2~-Aw$N?E+X1(8Z=bk5e0%=(_8q}H26z1L1oVA6h<)cv$@K%)?s`-%k;yWTtGVqNff_bxhrx z`aDgUR-AU6PM9vA?w+2S{{D#lk=i4-M`@3aKI(t;&!W@#2XOP{r#jhZc;?U=nc`}qmW6Qw84Pm-QgKIwfj^W?`C0!(GtphJ!*ktp({pRjIiG7j_k5oD zyzcqX^Z6Ih3y~M5FG64JebM^j){Bqxq3q=|l2|BOfk(nESBxQTU_j$MBEEAKO1pe*F50{gcKguTR;ZPJSByw77y@ zky)`{Nm!{|xw!IlW%IMpXOquipNl`Yf1dpO^$YEb<`?fTIbRyST>G-TN?4U&by`hc ztzI2ieeo6gD)!a#Yt+}WuialCef{x`_nX1Dpl?Or&V0M`?aOzz@9N*ZzGr`L_}a$aTqeoAtQ$^7X#;C+nLV!W*U=5gVl&of}gd8$Wn|=>G`% zvFAtIk9$8>f6{(x{q+5r|Fik$&7U8CvHVi~t0uvpfB)0|58_A} A0RR91 literal 0 HcmV?d00001 From fcc86d3a9d80da7f796697212deb144f94232699 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 8 Mar 2016 09:37:40 +0800 Subject: [PATCH 014/123] FIX: `PostgreSQLFallbackHandler` was bouncing in and out of readonly. --- .../postgresql_fallback_adapter.rb | 12 +++++++----- .../postgresql_fallback_adapter_spec.rb | 14 +++++++++++++- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index 1f54e8a5c1..bb2d75b605 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -39,7 +39,7 @@ class PostgreSQLFallbackHandler end Discourse.disable_readonly_mode - master = true + self.master = true end rescue => e if e.message.include?("could not connect to server") @@ -77,6 +77,10 @@ class PostgreSQLFallbackHandler end end + def verify? + !master && !running && !recently_checked? + end + private def config @@ -110,7 +114,7 @@ module ActiveRecord fallback_handler = ::PostgreSQLFallbackHandler.instance config = config.symbolize_keys - if !fallback_handler.master && !fallback_handler.running + if fallback_handler.verify? connection = postgresql_connection(config.dup.merge({ host: config[:replica_host], port: config[:replica_port] })) @@ -148,9 +152,7 @@ module ActiveRecord end def switch_back? - if !fallback_handler.master && !fallback_handler.running - fallback_handler.verify_master - end + fallback_handler.verify_master if fallback_handler.verify? end end end 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 0456f616d4..05e6b68cd9 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 @@ -13,8 +13,10 @@ describe ActiveRecord::ConnectionHandling do }).symbolize_keys! end + let(:postgresql_fallback_handler) { PostgreSQLFallbackHandler.instance } + after do - ::PostgreSQLFallbackHandler.instance.setup! + postgresql_fallback_handler.setup! end describe "#postgresql_fallback_connection" do @@ -58,18 +60,26 @@ describe ActiveRecord::ConnectionHandling do })).returns(@replica_connection) end + expect(postgresql_fallback_handler.master).to eq(true) + expect { ActiveRecord::Base.postgresql_fallback_connection(config) } .to raise_error(PG::ConnectionBad) expect{ ActiveRecord::Base.postgresql_fallback_connection(config) } .to change{ Discourse.readonly_mode? }.from(false).to(true) + expect(postgresql_fallback_handler.master).to eq(false) + with_multisite_db(multisite_db) do + expect(postgresql_fallback_handler.master).to eq(true) + expect { ActiveRecord::Base.postgresql_fallback_connection(multisite_config) } .to raise_error(PG::ConnectionBad) expect{ ActiveRecord::Base.postgresql_fallback_connection(multisite_config) } .to change{ Discourse.readonly_mode? }.from(false).to(true) + + expect(postgresql_fallback_handler.master).to eq(false) end ActiveRecord::Base.unstub(:postgresql_connection) @@ -92,6 +102,8 @@ describe ActiveRecord::ConnectionHandling do expect(Discourse.readonly_mode?).to eq(false) + expect(PostgreSQLFallbackHandler.instance.master).to eq(true) + expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) expect(ActiveRecord::Base.connection) From 00078a438b0dd480089518036d336d0d4cee995c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 8 Mar 2016 11:29:18 +0800 Subject: [PATCH 015/123] FIX: `FastImage#size` returns `nil` if it can't fetch the image size. --- lib/cooked_post_processor.rb | 11 +++++++---- spec/components/cooked_post_processor_spec.rb | 6 ++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 147824d829..ef1e3d6942 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -116,13 +116,16 @@ class CookedPostProcessor if w > 0 || h > 0 w = w.to_f h = h.to_f - original_width, original_height = get_size(img["src"]).map {|integer| integer.to_f} + + return unless original_image_size = get_size(img["src"]) + original_width, original_height = original_image_size.map(&:to_f) + if w > 0 ratio = w/original_width - return [w.floor, (original_height*ratio).floor] + [w.floor, (original_height*ratio).floor] else ratio = h/original_height - return [(original_width*ratio).floor, h.floor] + [(original_width*ratio).floor, h.floor] end end end @@ -149,7 +152,7 @@ class CookedPostProcessor return unless is_valid_image_url?(absolute_url) # we can *always* crawl our own images - return unless SiteSetting.crawl_images? || Discourse.store.has_been_uploaded?(url) + return unless SiteSetting.crawl_images || Discourse.store.has_been_uploaded?(url) @size_cache[url] ||= FastImage.size(absolute_url) rescue Zlib::BufError # FastImage.size raises BufError for some gifs diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 89d0c51e0a..8007134006 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -267,6 +267,12 @@ describe CookedPostProcessor do expect(cpp.get_size("http://foo.bar/image2.png")).to eq([100, 200]) end + it "returns nil if FastImage can't get the original size" do + Discourse.store.class.any_instance.expects(:has_been_uploaded?).returns(true) + FastImage.expects(:size).returns(nil) + expect(cpp.get_size("http://foo.bar/image3.png")).to eq(nil) + end + end end From 322ee3e17c1c59b209c2e9f04030ac484280f267 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 8 Mar 2016 11:38:26 +0800 Subject: [PATCH 016/123] Fix the build. --- lib/cooked_post_processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index ef1e3d6942..ea82b730a8 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -152,7 +152,7 @@ class CookedPostProcessor return unless is_valid_image_url?(absolute_url) # we can *always* crawl our own images - return unless SiteSetting.crawl_images || Discourse.store.has_been_uploaded?(url) + return unless SiteSetting.crawl_images? || Discourse.store.has_been_uploaded?(url) @size_cache[url] ||= FastImage.size(absolute_url) rescue Zlib::BufError # FastImage.size raises BufError for some gifs From d783a393ec28f503480e8ec8662de20ed64f46b5 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 8 Mar 2016 12:51:51 +0800 Subject: [PATCH 017/123] Fix randomly failing test. --- .../connection_adapters/postgresql_fallback_adapter.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb index bb2d75b605..857cc28b56 100644 --- a/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb +++ b/lib/active_record/connection_adapters/postgresql_fallback_adapter.rb @@ -74,6 +74,7 @@ class PostgreSQLFallbackHandler @master[db] = true @running[db] = false @mutex[db] = Mutex.new + @last_check[db] = nil end end From 2f00e2179f1d6157ef6c71a42c3e087899107a87 Mon Sep 17 00:00:00 2001 From: James Kiesel Date: Tue, 8 Mar 2016 09:25:14 +1300 Subject: [PATCH 018/123] Don't use unnecessary stubs; site setting language update --- config/locales/server.en.yml | 2 +- spec/models/user_option_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2cd7f6e473..1cf86ceafc 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1248,7 +1248,7 @@ en: default_email_private_messages: "Send an email when someone messages the user by default." default_email_direct: "Send an email when someone quotes/replies to/mentions or invites the user by default." default_email_mailing_list_mode: "Send an email for every new post by default." - disable_mailing_list_mode: "Disable sending an email for every new post for all users" + disable_mailing_list_mode: "Disallow users from enabling mailing list mode." default_email_always: "Send an email notification even when the user is active by default." default_email_previous_replies: "Include previous replies in emails by default." diff --git a/spec/models/user_option_spec.rb b/spec/models/user_option_spec.rb index b388cd8d68..50cfd6623d 100644 --- a/spec/models/user_option_spec.rb +++ b/spec/models/user_option_spec.rb @@ -18,7 +18,7 @@ describe UserOption do end - describe ".mailing_list_mode" do + describe "#mailing_list_mode" do let!(:forum_user) { Fabricate(:user) } let!(:mailing_list_user) { Fabricate(:user) } @@ -28,13 +28,13 @@ describe UserOption do end it "should return false when `SiteSetting.disable_mailing_list_mode` is enabled" do - SiteSetting.expects(:disable_mailing_list_mode).twice.returns(true) + SiteSetting.disable_mailing_list_mode = true expect(forum_user.user_option.mailing_list_mode).to eq(false) expect(mailing_list_user.user_option.mailing_list_mode).to eq(false) end it "should return the stored value when `SiteSetting.disable_mailing_list_mode` is disabled" do - SiteSetting.expects(:disable_mailing_list_mode).twice.returns(false) + SiteSetting.disable_mailing_list_mode = false expect(forum_user.user_option.mailing_list_mode).to eq(false) expect(mailing_list_user.user_option.mailing_list_mode).to eq(true) end From db781d3349112ed0130b73fa7594e74d923b2f40 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 8 Mar 2016 16:18:10 +0800 Subject: [PATCH 019/123] FIX: `LogsNotice` does not have `currentUser` attribute. --- .../javascripts/discourse/initializers/logs-notice.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/initializers/logs-notice.js.es6 b/app/assets/javascripts/discourse/initializers/logs-notice.js.es6 index 424d84298d..0118b540ab 100644 --- a/app/assets/javascripts/discourse/initializers/logs-notice.js.es6 +++ b/app/assets/javascripts/discourse/initializers/logs-notice.js.es6 @@ -9,9 +9,10 @@ export default { const siteSettings = container.lookup('site-settings:main'); const messageBus = container.lookup('message-bus:main'); const keyValueStore = container.lookup('key-value-store:main'); + const currentUser = container.lookup('current-user:main'); LogsNotice.reopenClass(Singleton, { createCurrent() { - return this.create({ messageBus, keyValueStore, siteSettings}); + return this.create({ messageBus, keyValueStore, siteSettings, currentUser }); } }); } From e9bff2e5502d3115f202eee53264138e09ce3636 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Tue, 8 Mar 2016 12:23:04 +0100 Subject: [PATCH 020/123] FIX: Add missing translation in groups page --- app/assets/javascripts/discourse/controllers/group.js.es6 | 5 +++++ app/assets/javascripts/discourse/templates/group.hbs | 4 ++-- config/locales/client.en.yml | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index 5f31424533..b7dc7a8057 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -4,6 +4,11 @@ var Tab = Em.Object.extend({ @computed('name') location(name) { return 'group.' + name; + }, + + @computed('name') + message(name) { + return I18n.t('groups.' + name); } }); diff --git a/app/assets/javascripts/discourse/templates/group.hbs b/app/assets/javascripts/discourse/templates/group.hbs index d778747227..6ad3e0ca3b 100644 --- a/app/assets/javascripts/discourse/templates/group.hbs +++ b/app/assets/javascripts/discourse/templates/group.hbs @@ -4,8 +4,8 @@
{{d-button action="loadNextVersion" icon="forward" title="post.revisions.controls.next" disabled=loadNextDisabled}} {{d-button action="loadLastVersion" icon="fast-forward" title="post.revisions.controls.last" disabled=loadLastDisabled}} - {{#if displayHide}} - {{d-button action="hideVersion" icon="trash-o" title="post.revisions.controls.hide" class="btn-danger" disabled=loading}} - {{/if}} - {{#if displayShow}} - {{d-button action="showVersion" icon="undo" title="post.revisions.controls.show" disabled=loading}} - {{/if}}
{{d-button action="displayInline" label="post.revisions.displays.inline.button" title="post.revisions.displays.inline.title" class=inlineClass}} @@ -85,5 +79,15 @@
{{{bodyDiff}}}
+ + {{#if displayRevert}} + {{d-button action="revertToVersion" icon="undo" label="post.revisions.controls.revert" class="btn-danger" disabled=loading}} + {{/if}} + {{#if displayHide}} + {{d-button action="hideVersion" icon="eye-slash" label="post.revisions.controls.hide" class="btn-danger" disabled=loading}} + {{/if}} + {{#if displayShow}} + {{d-button action="showVersion" icon="eye" label="post.revisions.controls.show" disabled=loading}} + {{/if}}
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index f9800e885b..fdf3dbb687 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -282,6 +282,55 @@ class PostsController < ApplicationController render nothing: true end + def revert + raise Discourse::NotFound unless guardian.is_staff? + + post_id = params[:id] || params[:post_id] + revision = params[:revision].to_i + raise Discourse::InvalidParameters.new(:revision) if revision < 2 + + post_revision = PostRevision.find_by(post_id: post_id, number: revision) + raise Discourse::NotFound unless post_revision + + post = find_post_from_params + raise Discourse::NotFound if post.blank? + + post_revision.post = post + guardian.ensure_can_see!(post_revision) + guardian.ensure_can_edit!(post) + return render_json_error(I18n.t('revert_version_same')) if post_revision.modifications["raw"].blank? && post_revision.modifications["title"].blank? && post_revision.modifications["category_id"].blank? + + topic = Topic.with_deleted.find(post.topic_id) + + changes = {} + changes[:raw] = post_revision.modifications["raw"][0] if post_revision.modifications["raw"].present? && post_revision.modifications["raw"][0] != post.raw + if post.is_first_post? + changes[:title] = post_revision.modifications["title"][0] if post_revision.modifications["title"].present? && post_revision.modifications["title"][0] != topic.title + changes[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present? && post_revision.modifications["category_id"][0] != topic.category.id + end + return render_json_error(I18n.t('revert_version_same')) unless changes.length > 0 + changes[:edit_reason] = "reverted to version ##{post_revision.number.to_i - 1}" + + revisor = PostRevisor.new(post, topic) + revisor.revise!(current_user, changes) + + return render_json_error(post) if post.errors.present? + return render_json_error(topic) if topic.errors.present? + + post_serializer = PostSerializer.new(post, scope: guardian, root: false) + post_serializer.draft_sequence = DraftSequence.current(current_user, topic.draft_key) + link_counts = TopicLink.counts_for(guardian, topic, [post]) + post_serializer.single_post_link_counts = link_counts[post.id] if link_counts.present? + + result = { post: post_serializer.as_json } + if post.is_first_post? + result[:topic] = BasicTopicSerializer.new(topic, scope: guardian, root: false).as_json if post_revision.modifications["title"].present? + result[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present? + end + + render_json_dump(result) + end + def bookmark post = find_post_from_params diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 16834c8944..110af93140 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1639,6 +1639,7 @@ en: last: "Last revision" hide: "Hide revision" show: "Show revision" + revert: "Revert to this revision" comparing_previous_to_current_out_of_total: "{{previous}} {{current}} / {{total}}" displays: inline: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e76ba73b4a..1497fd7fc8 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -204,6 +204,7 @@ en: top: "Top topics" posts: "Latest posts" too_late_to_edit: "That post was created too long ago. It can no longer be edited or deleted." + revert_version_same: "The current version is same as the version you are trying to revert to." excerpt_image: "image" diff --git a/config/routes.rb b/config/routes.rb index d28299a7bd..db70aabf1a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -386,6 +386,7 @@ Discourse::Application.routes.draw do get "revisions/:revision" => "posts#revisions", constraints: { revision: /\d+/ } put "revisions/:revision/hide" => "posts#hide_revision", constraints: { revision: /\d+/ } put "revisions/:revision/show" => "posts#show_revision", constraints: { revision: /\d+/ } + put "revisions/:revision/revert" => "posts#revert", constraints: { revision: /\d+/ } put "recover" collection do delete "destroy_many" diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index 14896e52d6..67392c081c 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -830,6 +830,79 @@ describe PostsController do end + describe 'revert post to a specific revision' do + include_examples 'action requires login', :put, :revert, post_id: 123, revision: 2 + + let(:post) { Fabricate(:post, user: logged_in_as, raw: "Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex") } + let(:post_revision) { Fabricate(:post_revision, post: post, modifications: {"raw" => ["this is original post body.", "this is edited post body."]}) } + let(:blank_post_revision) { Fabricate(:post_revision, post: post, modifications: {"edit_reason" => ["edit reason #1", "edit reason #2"]}) } + let(:same_post_revision) { Fabricate(:post_revision, post: post, modifications: {"raw" => ["Lorem ipsum dolor sit amet, cu nam libris tractatos, ancillae senserit ius ex", "this is edited post body."]}) } + + let(:revert_params) do + { + post_id: post.id, + revision: post_revision.number + } + end + let(:moderator) { Fabricate(:moderator) } + + describe 'when logged in as a regular user' do + let(:logged_in_as) { log_in } + + it "does not work" do + xhr :put, :revert, revert_params + expect(response).to_not be_success + end + end + + describe "when logged in as staff" do + let(:logged_in_as) { log_in(:moderator) } + + it "throws an exception when revision is < 2" do + expect { + xhr :put, :revert, post_id: post.id, revision: 1 + }.to raise_error(Discourse::InvalidParameters) + end + + it "fails when post_revision record is not found" do + xhr :put, :revert, post_id: post.id, revision: post_revision.number + 1 + expect(response).to_not be_success + end + + it "fails when post record is not found" do + xhr :put, :revert, post_id: post.id + 1, revision: post_revision.number + expect(response).to_not be_success + end + + it "fails when revision is blank" do + xhr :put, :revert, post_id: post.id, revision: blank_post_revision.number + + expect(response.status).to eq(422) + expect(JSON.parse(response.body)['errors']).to include(I18n.t('revert_version_same')) + end + + it "fails when revised version is same as current version" do + xhr :put, :revert, post_id: post.id, revision: same_post_revision.number + + expect(response.status).to eq(422) + expect(JSON.parse(response.body)['errors']).to include(I18n.t('revert_version_same')) + end + + it "works!" do + xhr :put, :revert, revert_params + expect(response).to be_success + end + + it "supports reverting posts in deleted topics" do + first_post = post.topic.ordered_posts.first + PostDestroyer.new(moderator, first_post).destroy + + xhr :put, :revert, revert_params + expect(response).to be_success + end + end + end + describe 'expandable embedded posts' do let(:post) { Fabricate(:post) } From 963d558bee8d7098ba47fded2f14236dbbac7252 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 11 Mar 2016 14:17:06 +1100 Subject: [PATCH 051/123] update message bus --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 93455abd85..8d2f214737 100644 --- a/Gemfile +++ b/Gemfile @@ -52,7 +52,7 @@ gem 'ember-source', '1.12.2' gem 'barber' gem 'babel-transpiler' -gem 'message_bus', '2.0.0.beta.4' +gem 'message_bus', '2.0.0.beta.5' gem 'rails_multisite' diff --git a/Gemfile.lock b/Gemfile.lock index 9b56f0feb6..1bfaa7ae12 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -157,7 +157,7 @@ GEM mail (2.6.3) mime-types (>= 1.16, < 3) memory_profiler (0.9.6) - message_bus (2.0.0.beta.4) + message_bus (2.0.0.beta.5) rack (>= 1.1.3) metaclass (0.0.4) method_source (0.8.2) @@ -438,7 +438,7 @@ DEPENDENCIES lru_redux mail memory_profiler - message_bus (= 2.0.0.beta.4) + message_bus (= 2.0.0.beta.5) mime-types minitest mocha From c07c47457524b35250755ae9b6d5ea6c10faf5b8 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 2 Mar 2016 22:01:48 +0800 Subject: [PATCH 052/123] FEATURE: Master-Slave Redis configuration with fallback and switch over. --- app/models/global_setting.rb | 3 + config/discourse_defaults.conf | 6 ++ config/initializers/001-redis.rb | 15 ++++ lib/discourse_redis.rb | 97 +++++++++++++++++++++++++ spec/components/discourse_redis_spec.rb | 76 +++++++++++++++++++ 5 files changed, 197 insertions(+) create mode 100644 spec/components/discourse_redis_spec.rb diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index b1bf927f2f..dd1c04820e 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -43,6 +43,8 @@ class GlobalSetting c = {} c[:host] = redis_host if redis_host c[:port] = redis_port if redis_port + c[:slave_host] = redis_slave_host if redis_slave_host + c[:slave_port] = redis_slave_port if redis_slave_port c[:password] = redis_password if redis_password.present? c[:db] = redis_db if redis_db != 0 c[:db] = 1 if Rails.env == "test" @@ -52,6 +54,7 @@ class GlobalSetting {host: host, port: port} end.to_a end + c[:connector] = DiscourseRedis::Connector c.freeze end end diff --git a/config/discourse_defaults.conf b/config/discourse_defaults.conf index 0ff3b8af81..d251d30efb 100644 --- a/config/discourse_defaults.conf +++ b/config/discourse_defaults.conf @@ -95,6 +95,12 @@ redis_host = localhost # redis server port redis_port = 6379 +# redis slave server address +redis_slave_host = + +# redis slave server port +redis_slave_port = + # redis database redis_db = 0 diff --git a/config/initializers/001-redis.rb b/config/initializers/001-redis.rb index 73340cc49d..0d1e2c6b03 100644 --- a/config/initializers/001-redis.rb +++ b/config/initializers/001-redis.rb @@ -1,3 +1,18 @@ +# https://github.com/redis/redis-rb/pull/591 +class Redis + class Client + alias_method :old_initialize, :initialize + + def initialize(options = {}) + old_initialize(options) + + if options.include?(:connector) && options[:connector].is_a?(Class) + @connector = options[:connector].new(@options) + end + end + end +end + if Rails.env.development? && ENV['DISCOURSE_FLUSH_REDIS'] puts "Flushing redis (development mode)" $redis.flushall diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index 560d5912f0..86dcb6de3c 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -3,6 +3,93 @@ # require_dependency 'cache' class DiscourseRedis + class FallbackHandler + include Singleton + + MASTER_LINK_STATUS = "master_link_status:up".freeze + + def initialize + @master = true + @running = false + @mutex = Mutex.new + @slave_config = DiscourseRedis.slave_config + end + + def verify_master + synchronize do + return if @running && !recently_checked? + @running = true + end + + Thread.new { initiate_fallback_to_master } + end + + def initiate_fallback_to_master + begin + slave_client = ::Redis::Client.new(@slave_config) + + if slave_client.call([:info]).split("\r\n").include?(MASTER_LINK_STATUS) + slave_client.call([:client, [:kill, 'type', 'normal']]) + Discourse.clear_readonly! + Discourse.request_refresh! + @master = true + end + ensure + @running = false + @last_checked = Time.zone.now + slave_client.disconnect + end + end + + def master + synchronize { @master } + end + + def master=(args) + synchronize { @master = args } + end + + def recently_checked? + if @last_checked + Time.zone.now > (@last_checked + 5.seconds) + else + false + end + end + + private + + def synchronize + @mutex.synchronize { yield } + end + end + + class Connector < Redis::Client::Connector + MASTER = 'master'.freeze + SLAVE = 'slave'.freeze + + def initialize(options) + super(options) + @slave_options = DiscourseRedis.slave_config(options) + @fallback_handler = DiscourseRedis::FallbackHandler.instance + end + + def resolve + begin + options = @options.dup + options.delete(:connector) + client = ::Redis::Client.new(options) + client.call([:role])[0] + @options + rescue Redis::ConnectionError, Redis::CannotConnectError => ex + return @slave_options if !@fallback_handler.master + @fallback_handler.master = false + raise ex + ensure + client.disconnect + end + end + end def self.raw_connection(config = nil) config ||= self.config @@ -13,11 +100,19 @@ class DiscourseRedis GlobalSetting.redis_config end + def self.slave_config(options = config) + options.dup.merge!({ host: options[:slave_host], port: options[:slave_port] }) + end + def initialize(config=nil) @config = config || DiscourseRedis.config @redis = DiscourseRedis.raw_connection(@config) end + def self.fallback_handler + @fallback_handler ||= DiscourseRedis::FallbackHandler.instance + end + def without_namespace # Only use this if you want to store and fetch data that's shared between sites @redis @@ -30,6 +125,8 @@ class DiscourseRedis unless Discourse.recently_readonly? STDERR.puts "WARN: Redis is in a readonly state. Performed a noop" end + + fallback_handler.verify_master if !fallback_handler.master Discourse.received_readonly! else raise ex diff --git a/spec/components/discourse_redis_spec.rb b/spec/components/discourse_redis_spec.rb new file mode 100644 index 0000000000..77d9bc6e56 --- /dev/null +++ b/spec/components/discourse_redis_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +describe DiscourseRedis do + let(:slave_host) { 'testhost' } + let(:slave_port) { 1234 } + + let(:config) do + DiscourseRedis.config.dup.merge({ + slave_host: 'testhost', slave_port: 1234, connector: DiscourseRedis::Connector + }) + end + + let(:fallback_handler) { DiscourseRedis::FallbackHandler.instance } + + context '.slave_host' do + it 'should return the right config' do + slave_config = DiscourseRedis.slave_config(config) + expect(slave_config[:host]).to eq(slave_host) + expect(slave_config[:port]).to eq(slave_port) + end + end + + context 'when redis connection is to a slave redis server' do + it 'should check the status of the master server' do + begin + fallback_handler.master = false + $redis.without_namespace.expects(:get).raises(Redis::CommandError.new("READONLY")) + fallback_handler.expects(:verify_master).once + $redis.get('test') + ensure + fallback_handler.master = true + end + end + end + + describe DiscourseRedis::Connector do + let(:connector) { DiscourseRedis::Connector.new(config) } + + it 'should return the master config when master is up' do + expect(connector.resolve).to eq(config) + end + + it 'should return the slave config when master is down' do + begin + Redis::Client.any_instance.expects(:call).raises(Redis::CannotConnectError).twice + expect { connector.resolve }.to raise_error(Redis::CannotConnectError) + + config = connector.resolve + + expect(config[:host]).to eq(slave_host) + expect(config[:port]).to eq(slave_port) + ensure + fallback_handler.master = true + end + end + end + + describe DiscourseRedis::FallbackHandler do + describe '#initiate_fallback_to_master' do + it 'should fallback to the master server once it is up' do + begin + fallback_handler.master = false + Redis::Client.any_instance.expects(:call).with([:info]).returns(DiscourseRedis::FallbackHandler::MASTER_LINK_STATUS) + Redis::Client.any_instance.expects(:call).with([:client, [:kill, 'type', 'normal']]) + + fallback_handler.initiate_fallback_to_master + + expect(fallback_handler.master).to eq(true) + expect(Discourse.recently_readonly?).to eq(false) + ensure + fallback_handler.master = true + end + end + end + end +end From 89b1998174f4a3d781e303b4bebd845460dc854e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 11 Mar 2016 15:07:07 +0800 Subject: [PATCH 053/123] Add default port for redis_slave. --- config/discourse_defaults.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/discourse_defaults.conf b/config/discourse_defaults.conf index d251d30efb..ee87099158 100644 --- a/config/discourse_defaults.conf +++ b/config/discourse_defaults.conf @@ -99,7 +99,7 @@ redis_port = 6379 redis_slave_host = # redis slave server port -redis_slave_port = +redis_slave_port = 6379 # redis database redis_db = 0 From eb6a17f587fff742f38e1604cce4b32dbb775039 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 11 Mar 2016 13:36:31 +0530 Subject: [PATCH 054/123] UX: show emoji in topic title on groups page --- app/assets/javascripts/discourse/models/group.js.es6 | 1 + .../discourse/templates/components/group-post.hbs | 2 +- app/serializers/group_post_serializer.rb | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index a34c6dccae..c2369960d5 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -130,6 +130,7 @@ const Group = Discourse.Model.extend({ return Discourse.ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => { return posts.map(p => { p.user = Discourse.User.create(p.user); + p.topic = Discourse.Topic.create(p.topic); return Em.Object.create(p); }); }); diff --git a/app/assets/javascripts/discourse/templates/components/group-post.hbs b/app/assets/javascripts/discourse/templates/components/group-post.hbs index 78abb6b538..0a379019e1 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post.hbs @@ -3,7 +3,7 @@
{{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}
{{format-date post.created_at leaveAgo="true"}} - {{unbound post.title}} + {{topic-link post.topic}} {{category-link post.category}}
diff --git a/app/serializers/group_post_serializer.rb b/app/serializers/group_post_serializer.rb index c9e37bdc07..ad4806d870 100644 --- a/app/serializers/group_post_serializer.rb +++ b/app/serializers/group_post_serializer.rb @@ -6,6 +6,7 @@ class GroupPostSerializer < ApplicationSerializer :url, :user_title, :user_long_name, + :topic, :category has_one :user, serializer: BasicUserSerializer, embed: :objects @@ -26,8 +27,11 @@ class GroupPostSerializer < ApplicationSerializer SiteSetting.enable_names? end + def topic + object.topic + end + def category object.topic.category end end - From 90fde5053d6309405d6ff6b5698fae7c871905cc Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 11 Mar 2016 17:25:24 +0800 Subject: [PATCH 055/123] FIX: Load Redis patch much earlier. --- config/application.rb | 1 + config/initializers/001-redis.rb | 15 --------------- lib/freedom_patches/redis.rb | 14 ++++++++++++++ 3 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 lib/freedom_patches/redis.rb diff --git a/config/application.rb b/config/application.rb index 18e838f55a..69a4531dd0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -147,6 +147,7 @@ module Discourse require 'discourse_redis' require 'logster/redis_store' + require 'freedom_patches/redis' # Use redis for our cache config.cache_store = DiscourseRedis.new_redis_store $redis = DiscourseRedis.new diff --git a/config/initializers/001-redis.rb b/config/initializers/001-redis.rb index 0d1e2c6b03..73340cc49d 100644 --- a/config/initializers/001-redis.rb +++ b/config/initializers/001-redis.rb @@ -1,18 +1,3 @@ -# https://github.com/redis/redis-rb/pull/591 -class Redis - class Client - alias_method :old_initialize, :initialize - - def initialize(options = {}) - old_initialize(options) - - if options.include?(:connector) && options[:connector].is_a?(Class) - @connector = options[:connector].new(@options) - end - end - end -end - if Rails.env.development? && ENV['DISCOURSE_FLUSH_REDIS'] puts "Flushing redis (development mode)" $redis.flushall diff --git a/lib/freedom_patches/redis.rb b/lib/freedom_patches/redis.rb new file mode 100644 index 0000000000..8bab1e7141 --- /dev/null +++ b/lib/freedom_patches/redis.rb @@ -0,0 +1,14 @@ +# https://github.com/redis/redis-rb/pull/591 +class Redis + class Client + alias_method :old_initialize, :initialize + + def initialize(options = {}) + old_initialize(options) + + if options.include?(:connector) && options[:connector].is_a?(Class) + @connector = options[:connector].new(@options) + end + end + end +end From bf209d8344e193e2bcbe28bdbc4bd5fecccf828a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 11 Mar 2016 18:54:01 +0800 Subject: [PATCH 056/123] FIX: Redis hostname may resolve to nothing. --- lib/discourse_redis.rb | 5 ++++- spec/components/discourse_redis_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index 86dcb6de3c..4bb3cd156d 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -81,7 +81,10 @@ class DiscourseRedis client = ::Redis::Client.new(options) client.call([:role])[0] @options - rescue Redis::ConnectionError, Redis::CannotConnectError => ex + rescue Redis::ConnectionError, Redis::CannotConnectError, RuntimeError => ex + # A consul service name may be deregistered for a redis container setup + raise ex if ex.class == RuntimeError && ex.message != "Name or service not known" + return @slave_options if !@fallback_handler.master @fallback_handler.master = false raise ex diff --git a/spec/components/discourse_redis_spec.rb b/spec/components/discourse_redis_spec.rb index 77d9bc6e56..c2d05c85db 100644 --- a/spec/components/discourse_redis_spec.rb +++ b/spec/components/discourse_redis_spec.rb @@ -53,6 +53,28 @@ describe DiscourseRedis do fallback_handler.master = true end end + + it "should return the slave config when master's hostname cannot be resolved" do + begin + error = RuntimeError.new('Name or service not known') + + Redis::Client.any_instance.expects(:call).raises(error).twice + expect { connector.resolve }.to raise_error(error) + + config = connector.resolve + + expect(config[:host]).to eq(slave_host) + expect(config[:port]).to eq(slave_port) + ensure + fallback_handler.master = true + end + end + + it "should raise the right error" do + error = RuntimeError.new('test error') + Redis::Client.any_instance.expects(:call).raises(error).twice + 2.times { expect { connector.resolve }.to raise_error(error) } + end end describe DiscourseRedis::FallbackHandler do From 6d84a8a1b3187101ebf184a42664f2310b2f8638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 11 Mar 2016 17:51:16 +0100 Subject: [PATCH 057/123] FIX: don't send out elided message in email notifications UX: improved details tag for elided messages --- config/locales/server.en.yml | 1 + lib/email/receiver.rb | 9 +++-- .../assets/javascripts/details_dialect.js | 2 ++ .../assets/stylesheets/details.scss | 35 +++++++++++++++++++ plugins/discourse-details/plugin.rb | 17 ++++----- spec/components/email/receiver_spec.rb | 4 +-- 6 files changed, 56 insertions(+), 12 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e76ba73b4a..8494bb3e0a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -50,6 +50,7 @@ en: emails: incoming: default_subject: "Incoming email from %{email}" + show_trimmed_content: "Show trimmed content" errors: empty_email_error: "Happens when the raw mail we received was blank." no_message_id_error: "Happens when the mail has no 'Message-Id' header." diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 27d05b9549..24eb6f7cff 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -56,9 +56,14 @@ module Email @incoming_email.update_columns(user_id: user.id) body, elided = select_body - body ||= "" - body << "\n\n[details=...]\n#{elided}\n[/details]" if elided.present? + + if elided.present? + body << "\n\n" << "
" << "\n" + body << "···" << "\n" + body << elided << "\n" + body << "
" << "\n" + end raise AutoGeneratedEmailError if is_auto_generated? raise NoBodyDetectedError if body.blank? && !@mail.has_attachments? diff --git a/plugins/discourse-details/assets/javascripts/details_dialect.js b/plugins/discourse-details/assets/javascripts/details_dialect.js index 4c80824d48..858f887a90 100644 --- a/plugins/discourse-details/assets/javascripts/details_dialect.js +++ b/plugins/discourse-details/assets/javascripts/details_dialect.js @@ -23,4 +23,6 @@ return text; }); + Discourse.Markdown.whiteListTag("details", "class", "elided"); + })(); diff --git a/plugins/discourse-details/assets/stylesheets/details.scss b/plugins/discourse-details/assets/stylesheets/details.scss index f1de007d2a..b183330956 100644 --- a/plugins/discourse-details/assets/stylesheets/details.scss +++ b/plugins/discourse-details/assets/stylesheets/details.scss @@ -7,6 +7,11 @@ details .lightbox-wrapper { display: none; } +details, +summary { + outline: none; +} + summary:first-of-type { cursor: pointer; display: block; @@ -36,3 +41,33 @@ summary::-webkit-details-marker { details .lazyYT-container { display: none; } + + +.elided { + color: dark-light-choose(scale-color($primary, $lightness: 65%), scale-color($secondary, $lightness: 35%)); + + summary:before { + content: '' !important; + } + + summary { + @include unselectable; + box-sizing: border-box; + margin: 0; + padding: 0; + color: #aaa; + background: #f1f1f1; + border: 1px solid #ddd; + width: 20px; + display: flex; + text-align: center; + vertical-align: middle; + line-height: 12px; + } + + summary:hover { + color: #222; + background: #d8d8d8; + border-color: #cdcdcd; + } +} diff --git a/plugins/discourse-details/plugin.rb b/plugins/discourse-details/plugin.rb index c028cf305d..41a1a61a71 100644 --- a/plugins/discourse-details/plugin.rb +++ b/plugins/discourse-details/plugin.rb @@ -1,6 +1,6 @@ # name: discourse-details # about: HTML5.1 Details polyfill for Discourse -# version: 0.3 +# version: 0.4 # authors: Régis Hanol # url: https://github.com/discourse/discourse/tree/master/plugins/discourse-details @@ -13,14 +13,15 @@ register_asset "stylesheets/details.scss" after_initialize do - # replace all details with their summary in emails Email::Styles.register_plugin_style do |fragment| - if SiteSetting.details_enabled - fragment.css("details").each do |details| - summary = details.css("summary")[0] - summary.name = "p" - details.replace(summary) - end + # remove all elided content + fragment.css("details.elided").each { |d| d.remove } + + # replace all details with their summary in emails + fragment.css("details").each do |details| + summary = details.css("summary")[0] + summary.name = "p" + details.replace(summary) end end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 164d60cb46..697231e433 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -118,7 +118,7 @@ describe Email::Receiver do it "removes the 'on , wrote' quoting line" do expect { process(:on_date_contact_wrote) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This is the actual reply.\n\n[details=...]\nOn Tue, Jan 14, 2016 at 0:42 AM, Bar Foo wrote:\n\n> This is the previous email.\n> And it had\n>\n> a lot\n>\n>\n> of lines ;)\n[/details]") + expect(topic.posts.last.raw).to eq("This is the actual reply.\n\n
\n···\nOn Tue, Jan 14, 2016 at 0:42 AM, Bar Foo wrote:\n\n> This is the previous email.\n> And it had\n>\n> a lot\n>\n>\n> of lines ;)\n
") end it "removes the 'Previous Replies' marker" do @@ -193,7 +193,7 @@ describe Email::Receiver do it "strips 'original message' context" do expect { process(:original_message) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This is a reply :)\n\n[details=...]\n---Original Message---\nThis part should not be included\n[/details]") + expect(topic.posts.last.raw).to eq("This is a reply :)\n\n
\n···\n---Original Message---\nThis part should not be included\n
") end it "supports attached images" do From 3d6deb0a72bd92344c65409b12bb420663f7b957 Mon Sep 17 00:00:00 2001 From: sethherr Date: Fri, 11 Mar 2016 11:37:17 -0600 Subject: [PATCH 058/123] Add an Email Services Template page - Link Admin Quick Start and basic install to services page - Provide templates for various email service providers --- docs/ADMIN-QUICK-START-GUIDE.md | 3 +-- docs/INSTALL-cloud.md | 11 +++----- docs/INSTALL-email.md | 45 +++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 docs/INSTALL-email.md diff --git a/docs/ADMIN-QUICK-START-GUIDE.md b/docs/ADMIN-QUICK-START-GUIDE.md index b955c7e17a..5c32d21124 100644 --- a/docs/ADMIN-QUICK-START-GUIDE.md +++ b/docs/ADMIN-QUICK-START-GUIDE.md @@ -73,8 +73,7 @@ Email is required for new account signups and notifications. **Test your email t - You got the test email? Great! **Read that email closely**, it has important email deliverability tips. - You didn't get the test email? This means your users probably aren't getting any signup or notification emails either. - -Email deliverability can be hard. We strongly recommend using dedicated email services like [SendGrid](https://sendgrid.com/), [SparkPost](https://www.sparkpost.com/), [MailGun](http://www.mailgun.com/), or [MailJet](https://www.mailjet.com/), which offer generous free plans that work fine for most communities. +- Email deliverability can be hard. Read [**Email Service Configuration**](https://github.com/discourse/discourse/blob/master/docs/INSTALL-email.md). If you'd like to enable *replying* to topics via email, [see this howto](https://meta.discourse.org/t/set-up-reply-via-email-support/14003). diff --git a/docs/INSTALL-cloud.md b/docs/INSTALL-cloud.md index 7a26d913e3..0efdb153bf 100644 --- a/docs/INSTALL-cloud.md +++ b/docs/INSTALL-cloud.md @@ -61,7 +61,7 @@ We recommend Nano because it's simple; just use your arrow keys to edit. - Set `DISCOURSE_HOSTNAME` to `discourse.example.com`, this means you want your Discourse available at `http://discourse.example.com/`. You'll need to update the DNS A record for this domain with the IP address of your server. -- Place your mail credentials in `DISCOURSE_SMTP_ADDRESS`, `DISCOURSE_SMTP_PORT`, `DISCOURSE_SMTP_USER_NAME`, `DISCOURSE_SMTP_PASSWORD`. Be sure you remove the comment `#` character and space from the front of these lines as necessary. +- Place your [mail credentials][mailconfig] in `DISCOURSE_SMTP_ADDRESS`, `DISCOURSE_SMTP_PORT`, `DISCOURSE_SMTP_USER_NAME`, `DISCOURSE_SMTP_PASSWORD`. Be sure you remove the comment `#` character and space from the front of these lines as necessary. - If you are using a 1 GB instance, set `UNICORN_WORKERS` to 2 and `db_shared_buffers` to 128MB so you have more memory room. @@ -75,11 +75,11 @@ Please be careful while editing and double check your work; YAML is _very_ sensi - Already have a mail server? Great. Use your existing mail server credentials. -- No existing mail server? Create a free account on [SendGrid][sg] (12k emails/month) [SparkPost][sp] (100k emails/month) [Mailgun][gun] (10k emails/month), [Mailjet][jet] (6k emails/month), and use the credentials provided in the dashboard. +- No existing mail server? Read [**Email Service Configuration**][mailconfig]. - For proper email deliverability, you must set correct SPF and DKIM records in your DNS. See your email provider instructions for specifics. -If you need to change or fix your email settings after bootstrapping, you must edit the `app.yml` file again and `./launcher rebuild app`, otherwise your changes will not take effect. +If you need to change or fix your email settings after bootstrapping, you must edit the `app.yml` file again and `./launcher rebuild app`, otherwise your changes will not take effect. If you didn't receive an email from your install, read [Register New Account and Become Admin][#register-new-account-and-become-admin] for troubleshooting. # Bootstrap Discourse @@ -173,11 +173,8 @@ Do you want... Help us improve this guide! Feel free to ask about it on [meta.discourse.org][meta], or even better, submit a pull request. [dd]: https://github.com/discourse/discourse_docker - [sp]: https://www.sparkpost.com/ [ssh]: https://help.github.com/articles/generating-ssh-keys [meta]: https://meta.discourse.org [do]: https://www.digitalocean.com/?refcode=5fa48ac82415 - [jet]: https://www.mailjet.com/pricing - [gun]: http://www.mailgun.com/ - [sg]: https://sendgrid.com/ [put]: http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html + [mailconfig]: https://github.com/discourse/discourse/blob/master/docs/INSTALL-email.md \ No newline at end of file diff --git a/docs/INSTALL-email.md b/docs/INSTALL-email.md new file mode 100644 index 0000000000..0b936f78a2 --- /dev/null +++ b/docs/INSTALL-email.md @@ -0,0 +1,45 @@ +## Email Is Important + +We strongly recommend using dedicated email services. + +The following are template configurations for email service providers who offer generous free plans that work for most communities. + +Use these values when you [edit the Discourse configuration](https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#edit-discourse-configuration) (`app.yml`). Replace the bracketed values with your values from the service. + +#### [SparkPost][sp] configuration (100k emails/month) + +```yml +DISCOURSE_SMTP_ADDRESS: smtp.sparkpostmail.com +DISCOURSE_SMTP_PORT: 587 +DISCOURSE_SMTP_USER_NAME: SMTP_Injection +DISCOURSE_SMTP_PASSWORD: [Any API key with Send via SMTP permission] +``` + +#### [SendGrid][sg] configuration (12k emails/month) + +```yml +DISCOURSE_SMTP_ADDRESS: smtp.sendgrid.net +DISCOURSE_SMTP_PORT: 587 +DISCOURSE_SMTP_USER_NAME: [SendGrid username] +DISCOURSE_SMTP_PASSWORD: [SendGrid password] +``` + +#### [Mailgun][gun] configuration (10k emails/month) + + +```yml +DISCOURSE_SMTP_ADDRESS: smtp.mailgun.org +DISCOURSE_SMTP_PORT: 587 +DISCOURSE_SMTP_USER_NAME: [SMTP credentials for your domain under Mailgun domains tab] +DISCOURSE_SMTP_PASSWORD: [SMTP credentials for your domain under Mailgun domains tab] +``` + +#### [Mailjet][jet] configuration (6k emails/month) + +Go to [My Account page](https://www.mailjet.com/account) and click on the ["SMTP and SEND API Settings"](https://www.mailjet.com/account/setup) link. + + + [sp]: https://www.sparkpost.com/ + [jet]: https://www.mailjet.com/pricing + [gun]: http://www.mailgun.com/ + [sg]: https://sendgrid.com/ From 841cec1bc13a6258f2b2f55ba9bad8da91847bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 11 Mar 2016 18:51:53 +0100 Subject: [PATCH 059/123] FIX: support emails in with weird encoding --- lib/email/receiver.rb | 4 +++- spec/components/email/receiver_spec.rb | 3 +++ spec/fixtures/emails/reply_with_weird_encoding.eml | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/emails/reply_with_weird_encoding.eml diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 24eb6f7cff..9906e7e49f 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -166,7 +166,9 @@ module Email def try_to_encode(string, encoding) string.encode("UTF-8", encoding) - rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError + rescue Encoding::InvalidByteSequenceError, + Encoding::UndefinedConversionError, + Encoding::ConverterNotFoundError nil end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 697231e433..9225e8277d 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -109,6 +109,9 @@ describe Email::Receiver do expect { process(:chinese_reply) }.to change { topic.posts.count } expect(topic.posts.last.raw).to eq("您好! 你今天好吗?") + + expect { process(:reply_with_weird_encoding) }.to change { topic.posts.count } + expect(topic.posts.last.raw).to eq("This is a reply with a weird encoding.") end it "prefers text over html" do diff --git a/spec/fixtures/emails/reply_with_weird_encoding.eml b/spec/fixtures/emails/reply_with_weird_encoding.eml new file mode 100644 index 0000000000..c764584e80 --- /dev/null +++ b/spec/fixtures/emails/reply_with_weird_encoding.eml @@ -0,0 +1,9 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <42@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain; charset=unicode-1-1-utf-7 + +This is a reply with a weird encoding. From 1006b1ba94d01be0de67893319745c23dd6e2431 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 11 Mar 2016 15:52:18 -0500 Subject: [PATCH 060/123] Various Plugin Enhancements and Extension Points --- .../discourse/helpers/plugin-outlet.js.es6 | 7 +++-- .../discourse/lib/plugin-api.js.es6 | 15 +++++++++-- .../discourse/templates/user/user.hbs | 3 +++ .../discourse/widgets/decorator-helper.js.es6 | 14 ++++++++++ .../discourse/widgets/emoji.js.es6 | 9 +++++++ app/models/user.rb | 27 +++++++++++++++++++ app/serializers/user_serializer.rb | 9 ++----- lib/plugin/instance.rb | 4 +++ lib/topic_view.rb | 12 ++++----- spec/models/user_spec.rb | 1 - 10 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 app/assets/javascripts/discourse/widgets/emoji.js.es6 diff --git a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 index 3df8b18c56..f7a85a460e 100644 --- a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 +++ b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 @@ -110,7 +110,7 @@ function buildConnectorCache() { _connectorCache[outletName].removeObject(viewClass); } else { if (!/\.raw$/.test(uniqueName)) { - viewClass = Em.View.extend({ classNames: [outletName + '-outlet', uniqueName] }); + viewClass = Ember.View.extend({ classNames: [outletName + '-outlet', uniqueName] }); } } @@ -172,8 +172,11 @@ Ember.HTMLBars._registerHelper('plugin-outlet', function(params, hash, options, // just shove it in. const viewClass = (childViews.length > 1) ? Ember.ContainerView : childViews[0]; + const newHash = viewInjections(env.data.view.container); + if (hash.tagName) { newHash.tagName = hash.tagName; } + delete options.fn; // we don't need the default template since we have a connector - env.helpers.view.helperFunction.call(this, [viewClass], viewInjections(env.data.view.container), options, env); + env.helpers.view.helperFunction.call(this, [viewClass], newHash, options, env); const cvs = env.data.view._childViews; if (childViews.length > 1 && cvs && cvs.length) { diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 947fae6f73..b8eeb0fe7f 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -5,7 +5,7 @@ import { addButton } from 'discourse/widgets/post-menu'; import { includeAttributes } from 'discourse/lib/transform-post'; import { addToolbarCallback } from 'discourse/components/d-editor'; import { addWidgetCleanCallback } from 'discourse/components/mount-widget'; -import { decorateWidget, changeSetting } from 'discourse/widgets/widget'; +import { createWidget, decorateWidget, changeSetting } from 'discourse/widgets/widget'; import { onPageChange } from 'discourse/lib/page-tracker'; import { preventCloak } from 'discourse/widgets/post-stream'; @@ -89,6 +89,8 @@ class PluginApi { const src = Discourse.Emoji.urlFor(emoji); return dec.h('img', { className: 'emoji', attributes: { src } }); }); + + iconBody = result.emoji.split('|').map(name => dec.attach('emoji', { name })); } if (result.text) { @@ -268,11 +270,20 @@ class PluginApi { preventCloak(postId) { preventCloak(postId); } + + /** + * Exposes the widget creating ability to plugins. Plugins can + * register their own plugins and attach them with decorators. + * See `createWidget` in `discourse/widgets/widget` for more info. + **/ + createWidget(name, args) { + return createWidget(name, args); + } } let _pluginv01; function getPluginApi(version) { - if (version === "0.1") { + if (version === "0.1" || version === "0.2") { if (!_pluginv01) { _pluginv01 = new PluginApi(version, Discourse.__container__); } diff --git a/app/assets/javascripts/discourse/templates/user/user.hbs b/app/assets/javascripts/discourse/templates/user/user.hbs index 3971824739..9c3b28877f 100644 --- a/app/assets/javascripts/discourse/templates/user/user.hbs +++ b/app/assets/javascripts/discourse/templates/user/user.hbs @@ -46,11 +46,14 @@ {{#if currentUser.staff}}
  • {{fa-icon "wrench"}}{{i18n 'admin.user.show_admin_profile'}}
  • {{/if}} + {{plugin-outlet "user-profile-controls" tagName="li"}} + {{#if collapsedInfo}} {{#if viewingSelf}}
  • {{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}
  • {{/if}} {{/if}} + diff --git a/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 b/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 index 6ea4eb7d2b..13db7ce836 100644 --- a/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 +++ b/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 @@ -22,6 +22,20 @@ class DecoratorHelper { **/ // h() is attached via `prototype` below + /** + * Attach another widget inside this one. + * + * ``` + * return helper.attach('widget-name'); + * ``` + */ + attach(name, attrs, state) { + attrs = attrs || this.widget.attrs; + state = state || this.widget.state; + + return this.widget.attach(name, attrs, state); + } + /** * Returns the model associated with this widget. When decorating * posts this will normally be the post. diff --git a/app/assets/javascripts/discourse/widgets/emoji.js.es6 b/app/assets/javascripts/discourse/widgets/emoji.js.es6 new file mode 100644 index 0000000000..6ab36c4692 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/emoji.js.es6 @@ -0,0 +1,9 @@ +import { createWidget } from 'discourse/widgets/widget'; + +export default createWidget('emoji', { + tagName: 'img.emoji', + + buildAttributes(attrs) { + return { src: Discourse.Emoji.urlFor(attrs.name) }; + }, +}); diff --git a/app/models/user.rb b/app/models/user.rb index fb865b213c..e0663db08e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -148,6 +148,33 @@ class User < ActiveRecord::Base User.where(username_lower: lower).blank? && !SiteSetting.reserved_usernames.split("|").include?(username) end + def self.plugin_staff_user_custom_fields + @plugin_staff_user_custom_fields ||= {} + end + + def self.register_plugin_staff_custom_field(custom_field_name, plugin) + plugin_staff_user_custom_fields[custom_field_name] = plugin + end + + def self.whitelisted_user_custom_fields(guardian) + fields = [] + + if SiteSetting.public_user_custom_fields.present? + fields += SiteSetting.public_user_custom_fields.split('|') + end + + if guardian.is_staff? + if SiteSetting.staff_user_custom_fields.present? + fields += SiteSetting.staff_user_custom_fields.split('|') + end + plugin_staff_user_custom_fields.each do |k, v| + fields << k if v.enabled? + end + end + + fields.uniq + end + def effective_locale if SiteSetting.allow_user_locale && self.locale.present? self.locale diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 730a23da74..b1acb42fe8 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -318,15 +318,10 @@ class UserSerializer < BasicUserSerializer end def custom_fields - fields = nil + fields = User.whitelisted_user_custom_fields(scope) if scope.can_edit?(object) - fields = DiscoursePluginRegistry.serialized_current_user_fields.to_a - end - - if SiteSetting.public_user_custom_fields.present? - fields ||= [] - fields += SiteSetting.public_user_custom_fields.split('|') + fields += DiscoursePluginRegistry.serialized_current_user_fields.to_a end if fields.present? diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index f13fd226a4..7fb3735de1 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -67,6 +67,10 @@ class Plugin::Instance klass.send(:define_method, "include_#{attr}?") { plugin.enabled? } end + def whitelist_staff_user_custom_field(field) + User.register_plugin_staff_custom_field(field, self) + end + # Extend a class but check that the plugin is enabled # for class methods use `add_class_method` def add_to_class(klass, attr, &block) diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 97b903ec50..9f84dd8063 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -54,13 +54,11 @@ class TopicView filter_posts(options) - if SiteSetting.public_user_custom_fields.present? && @posts - @user_custom_fields = User.custom_fields_for_ids(@posts.map(&:user_id), SiteSetting.public_user_custom_fields.split('|')) - end - - if @guardian.is_staff? && SiteSetting.staff_user_custom_fields.present? && @posts - @user_custom_fields ||= {} - @user_custom_fields.deep_merge!(User.custom_fields_for_ids(@posts.map(&:user_id), SiteSetting.staff_user_custom_fields.split('|'))) + if @posts + added_fields = User.whitelisted_user_custom_fields(@guardian) + if added_fields.present? + @user_custom_fields = User.custom_fields_for_ids(@posts.map(&:user_id), added_fields) + end end whitelisted_fields = TopicView.whitelisted_post_custom_fields(@user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 35a9e72725..84a992ae94 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -38,7 +38,6 @@ describe User do Jobs.expects(:enqueue).with(:send_system_message, user_id: user.id, message_type: 'welcome_user').never user.enqueue_welcome_message('welcome_user') end - end describe '.approve' do From 2cd6649ba05f10efda089ee026715c1b05e5833f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 11 Mar 2016 16:32:28 -0500 Subject: [PATCH 061/123] FIX: Was leaking `tagName` across plugin outlets --- app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 index f7a85a460e..6da20c3423 100644 --- a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 +++ b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 @@ -172,7 +172,7 @@ Ember.HTMLBars._registerHelper('plugin-outlet', function(params, hash, options, // just shove it in. const viewClass = (childViews.length > 1) ? Ember.ContainerView : childViews[0]; - const newHash = viewInjections(env.data.view.container); + const newHash = $.extend({}, viewInjections(env.data.view.container)); if (hash.tagName) { newHash.tagName = hash.tagName; } delete options.fn; // we don't need the default template since we have a connector From 090e097a4d1807790ebc9e3d34b5c18ef4901ad4 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 11 Mar 2016 23:08:22 -0800 Subject: [PATCH 062/123] Update INSTALL-email.md --- docs/INSTALL-email.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/INSTALL-email.md b/docs/INSTALL-email.md index 0b936f78a2..14ada0cd83 100644 --- a/docs/INSTALL-email.md +++ b/docs/INSTALL-email.md @@ -1,12 +1,12 @@ ## Email Is Important -We strongly recommend using dedicated email services. +We strongly recommend using a dedicated email service. Email server setup and maintenance is _very_ difficult even for experienced system administrators, and getting any part of the complex required email setup wrong means your email won't be delivered, or worse, delivered erratically. The following are template configurations for email service providers who offer generous free plans that work for most communities. -Use these values when you [edit the Discourse configuration](https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#edit-discourse-configuration) (`app.yml`). Replace the bracketed values with your values from the service. +Use these values when you [edit your Discourse `app.yml` configuration file](https://github.com/discourse/discourse/blob/master/docs/INSTALL-cloud.md#edit-discourse-configuration): -#### [SparkPost][sp] configuration (100k emails/month) +#### [SparkPost][sp] (100k emails/month) ```yml DISCOURSE_SMTP_ADDRESS: smtp.sparkpostmail.com @@ -15,7 +15,7 @@ DISCOURSE_SMTP_USER_NAME: SMTP_Injection DISCOURSE_SMTP_PASSWORD: [Any API key with Send via SMTP permission] ``` -#### [SendGrid][sg] configuration (12k emails/month) +#### [SendGrid][sg] (12k emails/month) ```yml DISCOURSE_SMTP_ADDRESS: smtp.sendgrid.net @@ -24,7 +24,7 @@ DISCOURSE_SMTP_USER_NAME: [SendGrid username] DISCOURSE_SMTP_PASSWORD: [SendGrid password] ``` -#### [Mailgun][gun] configuration (10k emails/month) +#### [Mailgun][gun] (10k emails/month) ```yml @@ -34,7 +34,7 @@ DISCOURSE_SMTP_USER_NAME: [SMTP credentials for your domain under Mailgun domain DISCOURSE_SMTP_PASSWORD: [SMTP credentials for your domain under Mailgun domains tab] ``` -#### [Mailjet][jet] configuration (6k emails/month) +#### [Mailjet][jet] (6k emails/month) Go to [My Account page](https://www.mailjet.com/account) and click on the ["SMTP and SEND API Settings"](https://www.mailjet.com/account/setup) link. From f3ef8dfd7b7ca761dea36cb0050f05ed8755e586 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 11 Mar 2016 23:09:19 -0800 Subject: [PATCH 063/123] Update INSTALL-email.md --- docs/INSTALL-email.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/INSTALL-email.md b/docs/INSTALL-email.md index 14ada0cd83..a1930f5ad8 100644 --- a/docs/INSTALL-email.md +++ b/docs/INSTALL-email.md @@ -1,4 +1,4 @@ -## Email Is Important +## Recommended Email Providers for Discourse We strongly recommend using a dedicated email service. Email server setup and maintenance is _very_ difficult even for experienced system administrators, and getting any part of the complex required email setup wrong means your email won't be delivered, or worse, delivered erratically. From a78392d218903900d5c66c15369c1afa05aa02f1 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Fri, 11 Mar 2016 23:15:05 -0800 Subject: [PATCH 064/123] Update INSTALL-cloud.md --- docs/INSTALL-cloud.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/INSTALL-cloud.md b/docs/INSTALL-cloud.md index 0efdb153bf..c71e61717e 100644 --- a/docs/INSTALL-cloud.md +++ b/docs/INSTALL-cloud.md @@ -61,7 +61,7 @@ We recommend Nano because it's simple; just use your arrow keys to edit. - Set `DISCOURSE_HOSTNAME` to `discourse.example.com`, this means you want your Discourse available at `http://discourse.example.com/`. You'll need to update the DNS A record for this domain with the IP address of your server. -- Place your [mail credentials][mailconfig] in `DISCOURSE_SMTP_ADDRESS`, `DISCOURSE_SMTP_PORT`, `DISCOURSE_SMTP_USER_NAME`, `DISCOURSE_SMTP_PASSWORD`. Be sure you remove the comment `#` character and space from the front of these lines as necessary. +- Place your [Email Server credentials][mailconfig] in `DISCOURSE_SMTP_ADDRESS`, `DISCOURSE_SMTP_PORT`, `DISCOURSE_SMTP_USER_NAME`, `DISCOURSE_SMTP_PASSWORD`. Be sure you remove the comment `#` character and space from the front of these lines as necessary. - If you are using a 1 GB instance, set `UNICORN_WORKERS` to 2 and `db_shared_buffers` to 128MB so you have more memory room. @@ -75,7 +75,7 @@ Please be careful while editing and double check your work; YAML is _very_ sensi - Already have a mail server? Great. Use your existing mail server credentials. -- No existing mail server? Read [**Email Service Configuration**][mailconfig]. +- No existing mail server? Check out our [**Recommended Email Providers for Discourse**][mailconfig]. - For proper email deliverability, you must set correct SPF and DKIM records in your DNS. See your email provider instructions for specifics. @@ -177,4 +177,4 @@ Help us improve this guide! Feel free to ask about it on [meta.discourse.org][me [meta]: https://meta.discourse.org [do]: https://www.digitalocean.com/?refcode=5fa48ac82415 [put]: http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html - [mailconfig]: https://github.com/discourse/discourse/blob/master/docs/INSTALL-email.md \ No newline at end of file + [mailconfig]: https://github.com/discourse/discourse/blob/master/docs/INSTALL-email.md From 129cc92ec96c75bef1185b99535f3a9847da4dc2 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Sun, 13 Mar 2016 13:54:03 +0000 Subject: [PATCH 065/123] Fixes the link count on the sidebar by only counting unique titles. --- app/assets/javascripts/discourse/widgets/post-gutter.js.es6 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 index 177d103213..f6bcbf94a8 100644 --- a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 @@ -41,7 +41,10 @@ export default createWidget('post-gutter', { } if (state.collapsed) { - const remaining = links.length - MAX_GUTTER_LINKS; + var differentTitles = new Set(); + links.forEach(function(x) { differentTitles.add(x.title); }); + const remaining = differentTitles.size - MAX_GUTTER_LINKS; + if (remaining > 0) { result.push(h('li', h('a.toggle-more', I18n.t('post.more_links', {count: remaining})))); } From e7a4900baf745a3a6f5b883cf561a2724e14e573 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Sun, 13 Mar 2016 14:31:03 +0000 Subject: [PATCH 066/123] Removed use of ES6 Set. --- .../javascripts/discourse/widgets/post-gutter.js.es6 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 index f6bcbf94a8..8a0d5acc96 100644 --- a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 @@ -41,9 +41,10 @@ export default createWidget('post-gutter', { } if (state.collapsed) { - var differentTitles = new Set(); - links.forEach(function(x) { differentTitles.add(x.title); }); - const remaining = differentTitles.size - MAX_GUTTER_LINKS; + var differentTitles = [], allTitles = []; + links.forEach(function(x) { allTitles.push(x.title); }); + differentTitles = allTitles.filter(function(item, i, ar){ return ar.indexOf(item) === i; }); + const remaining = differentTitles.length - MAX_GUTTER_LINKS; if (remaining > 0) { result.push(h('li', h('a.toggle-more', I18n.t('post.more_links', {count: remaining})))); From da4c96fad28c0db68f8e0e1ae8fa1ce82438cc9a Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Sun, 13 Mar 2016 14:37:15 +0000 Subject: [PATCH 067/123] Fixes scope problem. --- app/assets/javascripts/discourse/widgets/post-gutter.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 index 8a0d5acc96..f580b9aea5 100644 --- a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 @@ -43,7 +43,7 @@ export default createWidget('post-gutter', { if (state.collapsed) { var differentTitles = [], allTitles = []; links.forEach(function(x) { allTitles.push(x.title); }); - differentTitles = allTitles.filter(function(item, i, ar){ return ar.indexOf(item) === i; }); + differentTitles = allTitles.filter(function(item, iter, ar){ return ar.indexOf(item) === iter; }); const remaining = differentTitles.length - MAX_GUTTER_LINKS; if (remaining > 0) { From e00850a1abe67c8721a340dd37c2008e1c56a3d4 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Mon, 14 Mar 2016 23:27:02 +1100 Subject: [PATCH 068/123] FEATURE: implement before and after filters in search remove max_age and min_age supports - before:monday - after:june - before:2001 - before:2001-01-22 --- config/locales/server.en.yml | 2 +- lib/search.rb | 52 ++++++++++++++++++++++++++++++---- spec/components/search_spec.rb | 49 +++++++++++++++++++++++++++----- 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d3a05ffa94..7831d5e746 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2723,7 +2723,7 @@ en: category:foouser:foogroup:foobadge:foo in:likesin:postedin:watchingin:trackingin:private in:bookmarksin:first - posts_count:nummin_age:daysmax_age:days + posts_count:numbefore:days or dateafter:days or date

    Examples

    diff --git a/lib/search.rb b/lib/search.rb index a7c5c12121..fb8b989a41 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -94,6 +94,40 @@ class Search data end + def self.word_to_date(str) + + if str =~ /^[0-9]{1,3}$/ + return Time.zone.now.beginning_of_day.days_ago(str.to_i) + end + + if str =~ /^([12][0-9]{3})(-([0-1]?[0-9]))?(-([0-3]?[0-9]))?$/ + year = $1.to_i + month = $2 ? $3.to_i : 1 + day = $4 ? $5.to_i : 1 + + return if day==0 || month==0 || day > 31 || month > 12 + + return Time.zone.parse("#{year}-#{month}-#{day}") rescue nil + end + + if str.downcase == "yesterday" + return Time.zone.now.beginning_of_day.yesterday + end + + titlecase = str.downcase.titlecase + + if Date::DAYNAMES.include?(titlecase) + return Time.zone.now.beginning_of_week(str.downcase.to_sym) + end + + if idx = (Date::MONTHNAMES.find_index(titlecase) || + Date::ABBR_MONTHNAMES.find_index(titlecase)) + delta = Time.zone.now.month - idx + delta += 12 if delta < 0 + Time.zone.now.beginning_of_month.months_ago(delta) + end + end + def initialize(term, opts=nil) @opts = opts || {} @guardian = @opts[:guardian] || Guardian.new @@ -251,14 +285,20 @@ class Search end end - advanced_filter(/min_age:(\d+)/) do |posts,match| - n = match.to_i - posts.where("topics.created_at > ?", n.days.ago) + advanced_filter(/before:(.*)/) do |posts,match| + if date = Search.word_to_date(match) + posts.where("posts.created_at < ?", date) + else + posts + end end - advanced_filter(/max_age:(\d+)/) do |posts,match| - n = match.to_i - posts.where("topics.created_at < ?", n.days.ago) + advanced_filter(/after:(.*)/) do |posts,match| + if date = Search.word_to_date(match) + posts.where("posts.created_at > ?", date) + else + posts + end end private diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index 71d5da7cd9..42a4f1866d 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -408,15 +408,21 @@ describe Search do describe 'Advanced search' do - it 'supports min_age and max_age in:first user:' do - topic = Fabricate(:topic, created_at: 3.months.ago) - Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic) + it 'supports before and after in:first user:' do + + time = Time.zone.parse('2001-05-20 2:55') + freeze_time(time) + + topic = Fabricate(:topic) + Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic, created_at: time.months_ago(2)) _post = Fabricate(:post, raw: 'boom boom shake the room', topic: topic) - expect(Search.execute('test min_age:100').posts.length).to eq(1) - expect(Search.execute('test min_age:10').posts.length).to eq(0) - expect(Search.execute('test max_age:10').posts.length).to eq(1) - expect(Search.execute('test max_age:100').posts.length).to eq(0) + expect(Search.execute('test before:1').posts.length).to eq(1) + expect(Search.execute('test before:2001-04-20').posts.length).to eq(1) + expect(Search.execute('test before:2001').posts.length).to eq(0) + expect(Search.execute('test before:monday').posts.length).to eq(1) + + expect(Search.execute('test after:jan').posts.length).to eq(1) expect(Search.execute('test in:first').posts.length).to eq(1) expect(Search.execute('boom').posts.length).to eq(1) @@ -512,5 +518,34 @@ describe Search do Post.exec_sql("SELECT to_tsvector('bbb') @@ " << ts_query) end + context '#word_to_date' do + it 'parses relative dates correctly' do + time = Time.zone.parse('2001-02-20 2:55') + freeze_time(time) + + expect(Search.word_to_date('yesterday')).to eq(time.beginning_of_day.yesterday) + expect(Search.word_to_date('suNday')).to eq(Time.zone.parse('2001-02-18')) + expect(Search.word_to_date('thursday')).to eq(Time.zone.parse('2001-02-15')) + expect(Search.word_to_date('deCember')).to eq(Time.zone.parse('2000-12-01')) + expect(Search.word_to_date('deC')).to eq(Time.zone.parse('2000-12-01')) + expect(Search.word_to_date('january')).to eq(Time.zone.parse('2001-01-01')) + expect(Search.word_to_date('jan')).to eq(Time.zone.parse('2001-01-01')) + + + expect(Search.word_to_date('100')).to eq(time.beginning_of_day.days_ago(100)) + + expect(Search.word_to_date('invalid')).to eq(nil) + end + + it 'parses absolute dates correctly' do + expect(Search.word_to_date('2001-1-20')).to eq(Time.zone.parse('2001-01-20')) + expect(Search.word_to_date('2030-10-2')).to eq(Time.zone.parse('2030-10-02')) + expect(Search.word_to_date('2030-10')).to eq(Time.zone.parse('2030-10-01')) + expect(Search.word_to_date('2030')).to eq(Time.zone.parse('2030-01-01')) + expect(Search.word_to_date('2030-01-32')).to eq(nil) + expect(Search.word_to_date('10000')).to eq(nil) + end + end + end From 3e3aa919342eb71f88da8eec781661f054292b82 Mon Sep 17 00:00:00 2001 From: Andre Pereira Date: Mon, 14 Mar 2016 12:25:00 +0000 Subject: [PATCH 069/123] Refactor to use a single piece of logic --- .../discourse/widgets/post-gutter.js.es6 | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 index f580b9aea5..61d7f973e0 100644 --- a/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-gutter.js.es6 @@ -21,30 +21,27 @@ export default createWidget('post-gutter', { const seenTitles = {}; - - let i = 0; - while (i < links.length && result.length < toShow) { - const l = links[i++]; - + let titleCount = 0; + links.forEach(function(l) { let title = l.title; if (title && !seenTitles[title]) { seenTitles[title] = true; - const linkBody = [new RawHtml({ html: `${Discourse.Emoji.unescape(title)}` })]; - if (l.clicks) { - linkBody.push(h('span.badge.badge-notification.clicks', l.clicks.toString())); - } + titleCount++; + if (result.length < toShow) { + const linkBody = [new RawHtml({html: `${Discourse.Emoji.unescape(title)}`})]; + if (l.clicks) { + linkBody.push(h('span.badge.badge-notification.clicks', l.clicks.toString())); + } - const className = l.reflection ? 'inbound' : 'outbound'; - const link = h('a.track-link', { className, attributes: { href: l.url } }, linkBody); - result.push(h('li', link)); + const className = l.reflection ? 'inbound' : 'outbound'; + const link = h('a.track-link', {className, attributes: {href: l.url}}, linkBody); + result.push(h('li', link)); + } } - } + }); if (state.collapsed) { - var differentTitles = [], allTitles = []; - links.forEach(function(x) { allTitles.push(x.title); }); - differentTitles = allTitles.filter(function(item, iter, ar){ return ar.indexOf(item) === iter; }); - const remaining = differentTitles.length - MAX_GUTTER_LINKS; + const remaining = titleCount - MAX_GUTTER_LINKS; if (remaining > 0) { result.push(h('li', h('a.toggle-more', I18n.t('post.more_links', {count: remaining})))); From 9fc9f029eafec84b450620222dad8045f844eb89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 14 Mar 2016 16:53:53 +0100 Subject: [PATCH 070/123] fix title for posted notification --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 110af93140..5d7b3fa965 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1018,7 +1018,7 @@ en: group_mentioned: "

    {{username}} {{description}}

    " quoted: "

    {{username}} {{description}}

    " replied: "

    {{username}} {{description}}

    " - posted: "

    {{username}} {{description}}

    " + posted: "

    {{username}} {{description}}

    " edited: "

    {{username}} {{description}}

    " liked: "

    {{username}} {{description}}

    " liked_2: "

    {{username}}, {{username2}} {{description}}

    " From 4a3cb4a0005af4cd7bc36d3abbcb490d33b2fba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 14 Mar 2016 18:18:58 +0100 Subject: [PATCH 071/123] FIX: use MD5 of the email_string when there's no 'Message-Id' --- app/jobs/scheduled/poll_mailbox.rb | 1 - config/locales/server.en.yml | 7 ------- lib/email/receiver.rb | 6 +++--- spec/components/email/receiver_spec.rb | 14 ++++++-------- spec/fixtures/emails/missing_message_id.eml | 5 ++++- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index 26b8453eb5..28186c01dd 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -40,7 +40,6 @@ module Jobs message_template = case e when Email::Receiver::EmptyEmailError then :email_reject_empty when Email::Receiver::NoBodyDetectedError then :email_reject_empty - when Email::Receiver::NoMessageIdError then :email_reject_no_message_id 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/config/locales/server.en.yml b/config/locales/server.en.yml index 7831d5e746..0b2c6ae12b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1837,13 +1837,6 @@ en: Your reply was sent from a different email address than the one we expected, so we're not sure if this is the same person. Try sending from another email address, or contact a staff member. - email_reject_no_message_id: - subject_template: "[%{site_name}] Email issue -- No Message Id" - text_body_template: | - We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - - We couldn't find a `Message-Id` header in the email. Try sending from a different email address, or contact a staff member. - email_reject_no_account: subject_template: "[%{site_name}] Email issue -- Unknown Account" text_body_template: | diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 9906e7e49f..8372763a84 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -1,3 +1,4 @@ +require "digest" require_dependency "new_post_manager" require_dependency "post_action_creator" require_dependency "email/html_cleaner" @@ -9,7 +10,6 @@ module Email class ProcessingError < StandardError; end class EmptyEmailError < ProcessingError; end - class NoMessageIdError < ProcessingError; end class AutoGeneratedEmailError < ProcessingError; end class NoBodyDetectedError < ProcessingError; end class InactiveUserError < ProcessingError; end @@ -29,7 +29,7 @@ module Email raise EmptyEmailError if mail_string.blank? @raw_email = mail_string @mail = Mail.new(@raw_email) - raise NoMessageIdError if @mail.message_id.blank? + @message_id = @mail.message_id.presence || Digest::MD5.hexdigest(mail_string) end def process! @@ -42,7 +42,7 @@ module Email end def find_or_create_incoming_email - IncomingEmail.find_or_create_by(message_id: @mail.message_id) do |ie| + IncomingEmail.find_or_create_by(message_id: @message_id) do |ie| ie.raw = @raw_email ie.subject = subject ie.from_address = @from_email diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 9225e8277d..80413c3f5f 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -21,14 +21,6 @@ describe Email::Receiver do expect { Email::Receiver.new("") }.to raise_error(Email::Receiver::EmptyEmailError) end - it "raises an NoMessageIdError when 'mail_string' is not an email" do - expect { Email::Receiver.new("wat") }.to raise_error(Email::Receiver::NoMessageIdError) - end - - it "raises an NoMessageIdError when 'mail_string' is missing the message_id" do - expect { Email::Receiver.new(email(:missing_message_id)) }.to raise_error(Email::Receiver::NoMessageIdError) - end - it "raises an AutoGeneratedEmailError when the mail is auto generated" do expect { process(:auto_generated_precedence) }.to raise_error(Email::Receiver::AutoGeneratedEmailError) expect { process(:auto_generated_header) }.to raise_error(Email::Receiver::AutoGeneratedEmailError) @@ -65,6 +57,12 @@ describe Email::Receiver do let(:post) { create_post(topic: topic, user: user) } let!(:email_log) { Fabricate(:email_log, reply_key: reply_key, user: user, topic: topic, post: post) } + it "uses MD5 of 'mail_string' there is no message_id" do + mail_string = email(:missing_message_id) + expect { Email::Receiver.new(mail_string).process! }.to change { IncomingEmail.count } + expect(IncomingEmail.last.message_id).to eq(Digest::MD5.hexdigest(mail_string)) + end + it "raises a ReplyUserNotMatchingError when the email address isn't matching the one we sent the notification to" do expect { process(:reply_user_not_matching) }.to raise_error(Email::Receiver::ReplyUserNotMatchingError) end diff --git a/spec/fixtures/emails/missing_message_id.eml b/spec/fixtures/emails/missing_message_id.eml index 03405bed3c..0fbad2fadc 100644 --- a/spec/fixtures/emails/missing_message_id.eml +++ b/spec/fixtures/emails/missing_message_id.eml @@ -1,5 +1,8 @@ -From: Foo Bar +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com Date: Fri, 15 Jan 2016 00:12:43 +0100 Mime-Version: 1.0 Content-Type: text/plain Content-Transfer-Encoding: 7bit + +Some body From 4b7046efa43618fbbb9ec890974ba207545d2e17 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 14 Mar 2016 13:02:55 -0400 Subject: [PATCH 072/123] Remove unused commented out code --- .../javascripts/discourse/widgets/poster-name.js.es6 | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 index c9b2a45ab5..3c91a68d5c 100644 --- a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 +++ b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 @@ -56,15 +56,6 @@ export default createWidget('poster-name', { contents.push(h('span.user-title', titleContents)); } - // const cfs = attrs.userCustomFields; - // if (cfs) { - // _callbacks.forEach(cb => { - // const result = cb(cfs, attrs); - // if (result) { - // - // } - // }); - // } return contents; } }); From 7e97724341f41c6188667f4312eea221c0fb6e10 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 14 Mar 2016 13:21:58 -0400 Subject: [PATCH 073/123] FIX: On mobile jumping to replies was not working --- app/assets/javascripts/discourse/lib/url.js.es6 | 4 +--- .../javascripts/discourse/widgets/post.js.es6 | 13 ++++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index b5572ef0a7..0f3364ab32 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -11,9 +11,7 @@ const DiscourseURL = Ember.Object.createWithMixins({ return _jumpScheduled; }, - /** - Jumps to a particular post in the stream - **/ + // Jumps to a particular post in the stream jumpToPost(postNumber, opts) { const holderId = `#post_${postNumber}`; const offset = () => { diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index be6ed28ad1..f992101401 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -348,12 +348,20 @@ createWidget('post-article', { return rows; }, + _getTopicUrl() { + const post = this.findAncestorModel(); + return post ? post.get('topic.url') : null; + }, + toggleReplyAbove() { const replyPostNumber = this.attrs.reply_to_post_number; // jump directly on mobile if (this.attrs.mobileView) { - DiscourseURL.jumpToPost(replyPostNumber); + const topicUrl = this._getTopicUrl(); + if (topicUrl) { + DiscourseURL.routeTo(`${topicUrl}/${replyPostNumber}`); + } return Ember.RSVP.Promise.resolve(); } @@ -361,8 +369,7 @@ createWidget('post-article', { this.state.repliesAbove = []; return Ember.RSVP.Promise.resolve(); } else { - const post = this.findAncestorModel(); - const topicUrl = post ? post.get('topic.url') : null; + const topicUrl = this._getTopicUrl(); return this.store.find('post-reply-history', { postId: this.attrs.id }).then(posts => { this.state.repliesAbove = posts.map((p) => { p.shareUrl = `${topicUrl}/${p.post_number}`; From ff12b5bf57fef4b0283cc43d5c07540ba3e25e8b Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 14 Mar 2016 22:08:29 +0530 Subject: [PATCH 074/123] FIX: newly created categories were not showing up --- .../discourse/models/category.js.es6 | 4 ++++ .../routes/build-category-route.js.es6 | 17 +++++++++++++++-- app/controllers/categories_controller.rb | 11 ++++++++++- app/models/category.rb | 9 +++++++++ config/routes.rb | 2 ++ spec/models/category_spec.rb | 10 ++++++++++ 6 files changed, 50 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6 index 1a8261dd17..37cfd9f297 100644 --- a/app/assets/javascripts/discourse/models/category.js.es6 +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -286,6 +286,10 @@ Category.reopenClass({ return Discourse.ajax(`/c/${id}/show.json`); }, + reloadBySlug(slug, parentSlug) { + return parentSlug ? Discourse.ajax(`/c/${parentSlug}/${slug}/find_by_slug.json`) : Discourse.ajax(`/c/${slug}/find_by_slug.json`); + }, + search(term, opts) { var limit = 5; diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index f182d2fea5..b22694a20f 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -2,6 +2,8 @@ import { filterQueryParams, findTopicList } from 'discourse/routes/build-topic-r import { queryParams } from 'discourse/controllers/discovery-sortable'; import TopicList from 'discourse/models/topic-list'; import PermissionType from 'discourse/models/permission-type'; +import CategoryList from 'discourse/models/category-list'; +import Category from 'discourse/models/category'; // A helper function to create a category route with parameters export default (filter, params) => { @@ -9,7 +11,19 @@ export default (filter, params) => { queryParams, model(modelParams) { - return { category: Discourse.Category.findBySlug(modelParams.slug, modelParams.parentSlug) }; + const category = Category.findBySlug(modelParams.slug, modelParams.parentSlug); + if (!category) { + return Category.reloadBySlug(modelParams.slug, modelParams.parentSlug).then((atts) => { + if (modelParams.parentSlug) { + atts.category.parentCategory = Category.findBySlug(modelParams.parentSlug); + } + const record = this.store.createRecord('category', atts.category); + record.setupGroupsAndPermissions(); + this.site.updateCategory(record); + return { category: Category.findBySlug(modelParams.slug, modelParams.parentSlug) }; + }) + }; + return { category }; }, afterModel(model, transition) { @@ -38,7 +52,6 @@ export default (filter, params) => { _createSubcategoryList(category) { this._categoryList = null; if (Em.isNone(category.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) { - const CategoryList = require('discourse/models/category-list').default; return CategoryList.listForParent(this.store, category).then(list => this._categoryList = list); } diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index e99e39aa6d..5eea9591f0 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -2,7 +2,7 @@ require_dependency 'category_serializer' class CategoriesController < ApplicationController - before_filter :ensure_logged_in, except: [:index, :show, :redirect] + before_filter :ensure_logged_in, except: [:index, :show, :redirect, :find_by_slug] before_filter :fetch_category, only: [:show, :update, :destroy] before_filter :initialize_staff_action_logger, only: [:create, :update, :destroy] skip_before_filter :check_xhr, only: [:index, :redirect] @@ -153,6 +153,15 @@ class CategoriesController < ApplicationController render json: success_json end + def find_by_slug + params.require(:category_slug) + @category = Category.find_by_slug(params[:category_slug], params[:parent_category_slug]) + guardian.ensure_can_see!(@category) + + @category.permission = CategoryGroup.permission_types[:full] if Category.topic_create_allowed(guardian).where(id: @category.id).exists? + render_serialized(@category, CategorySerializer) + end + private def required_param_keys diff --git a/app/models/category.rb b/app/models/category.rb index dfb3eca617..5dcaaa46bf 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -450,6 +450,15 @@ SQL def publish_discourse_stylesheet DiscourseStylesheets.cache.clear end + + def self.find_by_slug(category_slug, parent_category_slug=nil) + if parent_category_slug + parent_category_id = self.where(slug: parent_category_slug, parent_category_id: nil).pluck(:id).first + self.where(slug: category_slug, parent_category_id: parent_category_id).first + else + self.where(slug: category_slug, parent_category_id: nil).first + end + end end # == Schema Information diff --git a/config/routes.rb b/config/routes.rb index db70aabf1a..a3e5a772d9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -430,6 +430,8 @@ Discourse::Application.routes.draw do put "category/:category_id/slug" => "categories#update_slug" get "c/:id/show" => "categories#show" + get "c/:category_slug/find_by_slug" => "categories#find_by_slug" + get "c/:parent_category_slug/:category_slug/find_by_slug" => "categories#find_by_slug" get "c/:category.rss" => "list#category_feed", format: :rss get "c/:parent_category/:category.rss" => "list#category_feed", format: :rss get "c/:category" => "list#category_latest" diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index ff253badbb..4506c0bbe3 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -583,4 +583,14 @@ describe Category do end end + describe "find_by_slug" do + it "finds with category and sub category" do + category = Fabricate(:category, slug: 'awesome-category') + sub_category = Fabricate(:category, parent_category_id: category.id, slug: 'awesome-sub-category') + + expect(Category.find_by_slug('awesome-category')).to eq(category) + expect(Category.find_by_slug('awesome-sub-category', 'awesome-category')).to eq(sub_category) + end + end + end From 621f7e0a658c35e40178d7c0ac1bff526621504d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 14 Mar 2016 14:48:30 -0400 Subject: [PATCH 075/123] FIX: Replace emoji in local oneboxes --- lib/onebox/engine/discourse_local_onebox.rb | 2 +- lib/onebox/templates/discourse_topic_onebox.hbs | 2 +- .../onebox/engine/discourse_local_onebox_spec.rb | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/onebox/engine/discourse_local_onebox.rb b/lib/onebox/engine/discourse_local_onebox.rb index 6708c98d85..9ac748e27f 100644 --- a/lib/onebox/engine/discourse_local_onebox.rb +++ b/lib/onebox/engine/discourse_local_onebox.rb @@ -93,7 +93,7 @@ module Onebox quote = post.excerpt(SiteSetting.post_onebox_maxlength) args = { original_url: url, - title: topic.title, + title: PrettyText.unescape_emoji(topic.title), avatar: PrettyText.avatar_img(topic.user.avatar_template, 'tiny'), posts_count: topic.posts_count, last_post: FreedomPatches::Rails4.time_ago_in_words(topic.last_posted_at, false, scope: :'datetime.distance_in_words_verbose'), diff --git a/lib/onebox/templates/discourse_topic_onebox.hbs b/lib/onebox/templates/discourse_topic_onebox.hbs index a070f974f1..b9ca0aff51 100644 --- a/lib/onebox/templates/discourse_topic_onebox.hbs +++ b/lib/onebox/templates/discourse_topic_onebox.hbs @@ -2,7 +2,7 @@
    {{{avatar}}} - {{title}} {{{category_html}}} + {{{title}}} {{{category_html}}}
    {{{quote}}}
    diff --git a/spec/components/onebox/engine/discourse_local_onebox_spec.rb b/spec/components/onebox/engine/discourse_local_onebox_spec.rb index 627eaba4b6..3646e97290 100644 --- a/spec/components/onebox/engine/discourse_local_onebox_spec.rb +++ b/spec/components/onebox/engine/discourse_local_onebox_spec.rb @@ -60,11 +60,16 @@ describe Onebox::Engine::DiscourseLocalOnebox do end it "returns a link if not allowed to see the post" do - url = "#{topic.url}" + url = topic.url Guardian.any_instance.stubs(:can_see?).returns(false) expect(Onebox.preview(url).to_s).to eq("#{url}") end + it "replaces emoji in the title" do + topic.update_column(:title, "Who wants to eat a :hamburger:") + expect(Onebox.preview(topic.url).to_s).to match(/hamburger.png/) + end + it "returns some onebox goodness if post exists and can be seen" do SiteSetting.external_system_avatars_enabled = false url = "#{topic.url}" @@ -94,7 +99,7 @@ describe Onebox::Engine::DiscourseLocalOnebox do expect(html).to eq("") end end - + context "When deployed to a subfolder" do let(:base_url) { "http://test.localhost/subfolder" } let(:base_uri) { "/subfolder" } From f46ced6fc28d624bd6daf0553d4a773b44ca2d27 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 14 Mar 2016 14:52:40 -0400 Subject: [PATCH 076/123] FIX: missing semi colon --- .../javascripts/discourse/routes/build-category-route.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index b22694a20f..14bc77f8a5 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -21,7 +21,7 @@ export default (filter, params) => { record.setupGroupsAndPermissions(); this.site.updateCategory(record); return { category: Category.findBySlug(modelParams.slug, modelParams.parentSlug) }; - }) + }); }; return { category }; }, From d0b9b226486093e3dfe76659152d9012f3d4b970 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 14 Mar 2016 15:44:57 -0400 Subject: [PATCH 077/123] FIX: delete spammer button was never shown in flag modal --- app/assets/javascripts/discourse/controllers/flag.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/flag.js.es6 b/app/assets/javascripts/discourse/controllers/flag.js.es6 index c598bc682b..5537c7bb6b 100644 --- a/app/assets/javascripts/discourse/controllers/flag.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag.js.es6 @@ -141,7 +141,7 @@ export default Ember.Controller.extend(ModalFunctionality, { fetchUserDetails() { if (Discourse.User.currentProp('staff') && this.get('model.username')) { const AdminUser = require('admin/models/admin-user').default; - AdminUser.find(this.get('model.id')).then(user => this.set('userDetails', user)); + AdminUser.find(this.get('model.user_id')).then(user => this.set('userDetails', user)); } } From 84ab7fdcc7700f5b3825705acaf44bdb75820b2a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 14 Mar 2016 16:23:40 -0400 Subject: [PATCH 078/123] FIX: post admin menu was hidden on mobile --- app/assets/stylesheets/mobile/topic-post.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index ac5adc6de8..fdcc9e192e 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -100,7 +100,6 @@ button { bottom: 0; left: 135px; z-index: 1000; - display: none; h3 { margin-top: 0; From e09e80702798b2fea15cefb23aa6502a3c4bee6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 14 Mar 2016 22:21:18 +0100 Subject: [PATCH 079/123] FIX: skip validation when replying via email for staged users --- lib/email/receiver.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 8372763a84..340b0447d1 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -74,7 +74,11 @@ module Email message = SubscriptionMailer.send(action, user) Email::Sender.new(message, :subscription).send elsif post = find_related_post - create_reply(user: user, raw: body, post: post, topic: post.topic) + create_reply(user: user, + raw: body, + post: post, + topic: post.topic, + skip_validations: user.staged?) else destination = destinations.first From dab6b9ba6eb63140ea54c899d7a8cb909fca975e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 14 Mar 2016 22:25:44 +0100 Subject: [PATCH 080/123] remove light gray in elided part --- plugins/discourse-details/assets/stylesheets/details.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/discourse-details/assets/stylesheets/details.scss b/plugins/discourse-details/assets/stylesheets/details.scss index b183330956..bbe4ab9013 100644 --- a/plugins/discourse-details/assets/stylesheets/details.scss +++ b/plugins/discourse-details/assets/stylesheets/details.scss @@ -44,7 +44,6 @@ details .lazyYT-container { .elided { - color: dark-light-choose(scale-color($primary, $lightness: 65%), scale-color($secondary, $lightness: 35%)); summary:before { content: '' !important; From 0cbeda84144f183f93eec45ea631e38a8a833961 Mon Sep 17 00:00:00 2001 From: scossar Date: Mon, 14 Mar 2016 16:18:19 -0700 Subject: [PATCH 081/123] add site setting for setting locale from header --- app/controllers/application_controller.rb | 2 +- config/locales/server.en.yml | 1 + config/site_settings.yml | 3 +++ .../allow_user_locale_enabled_validator.rb | 18 ++++++++++++++++++ .../controllers/application_controller_spec.rb | 5 +++-- 5 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 lib/validators/allow_user_locale_enabled_validator.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8a09c8fbee..c4ad643c74 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -165,7 +165,7 @@ class ApplicationController < ActionController::Base def set_locale if !current_user - if SiteSetting.allow_user_locale + if SiteSetting.set_locale_from_accept_language_header I18n.locale = locale_from_header else I18n.locale = SiteSetting.default_locale diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 7831d5e746..5396a43a13 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -787,6 +787,7 @@ en: delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days." default_locale: "The default language of this Discourse instance (ISO 639-1 Code)" allow_user_locale: "Allow users to choose their own language interface preference" + set_locale_from_accept_language_header: "set interface language for anonymous users from their web browser's language headers." min_post_length: "Minimum allowed post length in characters" min_first_post_length: "Minimum allowed first post (topic body) length in characters" min_private_message_post_length: "Minimum allowed post length in characters for messages" diff --git a/config/site_settings.yml b/config/site_settings.yml index 7247038e0e..fe5f85ba99 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -64,6 +64,9 @@ basic: allow_user_locale: client: true default: false + set_locale_from_accept_language_header: + default: false + validator: "AllowUserLocaleEnabledValidator" suggested_topics: client: true default: 5 diff --git a/lib/validators/allow_user_locale_enabled_validator.rb b/lib/validators/allow_user_locale_enabled_validator.rb new file mode 100644 index 0000000000..9f36c32d57 --- /dev/null +++ b/lib/validators/allow_user_locale_enabled_validator.rb @@ -0,0 +1,18 @@ +class AllowUserLocaleEnabledValidator + + def initialize(opts={}) + @opts = opts + end + + def valid_value?(val) + # only validate when enabling setting locale from headers + return true if val == "f" + # ensure that allow_user_locale is enabled + SiteSetting.allow_user_locale + end + + def error_message + "You must first enable 'allow user locale' before enabling this setting." + end + +end \ No newline at end of file diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 49ddc8f814..84e770e439 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -148,10 +148,11 @@ describe TopicsController do end end - context "allow_user_locale enabled" do + context "set_locale_from_accept_language_header enabled" do context "accept-language header differs from default locale" do before do SiteSetting.stubs(:allow_user_locale).returns(true) + SiteSetting.stubs(:set_locale_from_accept_language_header).returns(true) SiteSetting.stubs(:default_locale).returns("en") set_accept_language("fr") end @@ -178,7 +179,7 @@ describe TopicsController do context "the preferred locale includes a region" do it "returns the locale and region separated by an underscore" do - SiteSetting.stubs(:allow_user_locale).returns(true) + SiteSetting.stubs(:set_locale_from_accept_language_header).returns(true) SiteSetting.stubs(:default_locale).returns("en") set_accept_language("zh-CN") From 91a2750084a474a62fe2a61c43b91f6f9396b2d0 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 14 Mar 2016 20:31:50 -0400 Subject: [PATCH 082/123] Advance draft sequence in PostRevisor if edit contains no changes Simplest version of the server-side fix for https://meta.discourse.org/t/draft-not-cleared-properly-when-empty-edit- of-post-is-saved/40939 --- lib/post_revisor.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index 873be66e0d..ccd16c0878 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -154,6 +154,7 @@ class PostRevisor POST_TRACKED_FIELDS.each do |field| return true if @fields.has_key?(field) && @fields[field] != @post.send(field) end + advance_draft_sequence false end From 15429370dad5f41a98821f37d9d1165aeced3580 Mon Sep 17 00:00:00 2001 From: tomasibarrab Date: Tue, 15 Mar 2016 02:35:29 -0700 Subject: [PATCH 083/123] Fix length for hashtags, increased to 101 characters. --- .../javascripts/discourse/dialects/category_hashtag_dialect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/dialects/category_hashtag_dialect.js b/app/assets/javascripts/discourse/dialects/category_hashtag_dialect.js index 6aec1319d7..27ebe296fb 100644 --- a/app/assets/javascripts/discourse/dialects/category_hashtag_dialect.js +++ b/app/assets/javascripts/discourse/dialects/category_hashtag_dialect.js @@ -4,7 +4,7 @@ **/ Discourse.Dialect.inlineRegexp({ start: '#', - matcher: /^#([\w-:]{1,50})/i, + matcher: /^#([\w-:]{1,101})/i, spaceOrTagBoundary: true, emitter: function(matches) { From 3e32393ab631905146eea8934fc53c5adc5413d2 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 15 Mar 2016 14:43:52 +0530 Subject: [PATCH 084/123] FIX: do not allow normal users to wiki edit-expired posts --- lib/guardian/post_guardian.rb | 9 ++++++++- spec/components/guardian_spec.rb | 22 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 8635bc6c1f..9c07db3380 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -175,7 +175,14 @@ module PostGuardian def can_wiki?(post) return false unless authenticated? - is_staff? || @user.has_trust_level?(TrustLevel[4]) || (@user.has_trust_level?(SiteSetting.min_trust_to_allow_self_wiki) && is_my_own?(post)) + return true if is_staff? || @user.has_trust_level?(TrustLevel[4]) + + if @user.has_trust_level?(SiteSetting.min_trust_to_allow_self_wiki) && is_my_own?(post) + return false if post.hidden? + return !post.edit_time_limit_expired? + end + + false end def can_change_post_type? diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 7951409e00..59dd7bdc21 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -2098,7 +2098,7 @@ describe Guardian do end describe 'can_wiki?' do - let(:post) { build(:post) } + let(:post) { build(:post, created_at: 1.minute.ago) } it 'returns false for regular user' do expect(Guardian.new(coding_horror).can_wiki?(post)).to be_falsey @@ -2127,5 +2127,25 @@ describe Guardian do it 'returns true for trust_level_4 user' do expect(Guardian.new(trust_level_4).can_wiki?(post)).to be_truthy end + + context 'post is older than post_edit_time_limit' do + let(:old_post) { build(:post, user: trust_level_2, created_at: 6.minutes.ago) } + before do + SiteSetting.min_trust_to_allow_self_wiki = 2 + SiteSetting.post_edit_time_limit = 5 + end + + it 'returns false when user satisfies trust level and owns the post' do + expect(Guardian.new(trust_level_2).can_wiki?(old_post)).to be_falsey + end + + it 'returns true for admin user' do + expect(Guardian.new(admin).can_wiki?(old_post)).to be_truthy + end + + it 'returns true for trust_level_4 user' do + expect(Guardian.new(trust_level_4).can_wiki?(post)).to be_truthy + end + end end end From f81153716624700fe8d26fd312d867c81c15811c Mon Sep 17 00:00:00 2001 From: sethherr Date: Tue, 15 Mar 2016 09:48:17 -0500 Subject: [PATCH 085/123] Fix link in INSTALL-cloud email troubleshooting --- docs/INSTALL-cloud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/INSTALL-cloud.md b/docs/INSTALL-cloud.md index c71e61717e..0bdcb17685 100644 --- a/docs/INSTALL-cloud.md +++ b/docs/INSTALL-cloud.md @@ -79,7 +79,7 @@ Please be careful while editing and double check your work; YAML is _very_ sensi - For proper email deliverability, you must set correct SPF and DKIM records in your DNS. See your email provider instructions for specifics. -If you need to change or fix your email settings after bootstrapping, you must edit the `app.yml` file again and `./launcher rebuild app`, otherwise your changes will not take effect. If you didn't receive an email from your install, read [Register New Account and Become Admin][#register-new-account-and-become-admin] for troubleshooting. +If you need to change or fix your email settings after bootstrapping, you must edit the `app.yml` file again and `./launcher rebuild app`, otherwise your changes will not take effect. If you didn't receive an email from your install, read [Register New Account and Become Admin](#register-new-account-and-become-admin) for troubleshooting. # Bootstrap Discourse From e4bc0de537a9e84401c43dafd75e62aec4276298 Mon Sep 17 00:00:00 2001 From: sethherr Date: Tue, 15 Mar 2016 10:30:06 -0500 Subject: [PATCH 086/123] Add information about updating sending domain --- docs/INSTALL-email.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/INSTALL-email.md b/docs/INSTALL-email.md index a1930f5ad8..db40eea4e3 100644 --- a/docs/INSTALL-email.md +++ b/docs/INSTALL-email.md @@ -15,6 +15,12 @@ DISCOURSE_SMTP_USER_NAME: SMTP_Injection DISCOURSE_SMTP_PASSWORD: [Any API key with Send via SMTP permission] ``` +If not using **the exact** domain you verified (e.g. you're using a subdomain of it), you must change the default `from` email to match the sending domain. Uncomment (and update with your sending domain) this line in `app.yml`: + +```yml +- exec: rails r "SiteSetting.notification_email='info@unconfigured.discourse.org'" +``` + #### [SendGrid][sg] (12k emails/month) ```yml From ee5acf194864333e8fe83c8d8a7d618b1ade5075 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 15 Mar 2016 21:11:18 +0530 Subject: [PATCH 087/123] Update Translations --- config/locales/client.es.yml | 79 +++++++---- config/locales/client.fi.yml | 18 ++- config/locales/client.fr.yml | 28 +++- config/locales/client.nl.yml | 17 ++- config/locales/client.ru.yml | 35 +---- config/locales/client.sk.yml | 46 +++---- config/locales/client.zh_CN.yml | 28 +++- config/locales/server.ar.yml | 145 ++++++-------------- config/locales/server.fi.yml | 38 ++++-- config/locales/server.nl.yml | 226 +++++++++++++++++++++++++++++++- config/locales/server.sk.yml | 100 ++------------ public/403.zh_CN.html | 6 +- 12 files changed, 459 insertions(+), 307 deletions(-) diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 1bc738d842..38b1ad5c37 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -179,6 +179,8 @@ es: more: "Más" less: "Menos" never: "nunca" + every_30_minutes: "cada 30 minutos" + every_hour: "cada hora" daily: "cada día" weekly: "cada semana" every_two_weeks: "cada dos semanas" @@ -269,7 +271,7 @@ es: one: "Este tema tiene 1 post esperando aprobación" other: "Este tema tiene {{count}} posts esperando aprobación" confirm: "Guardar Cambios" - delete_prompt: "¿Seguro que quieres eliminar a %{username}? Esto eliminará todos sus posts y bloqueará su email y dirección IP." + delete_prompt: "¿Seguro que quieres eliminar a %{username}? Se eliminarán todos sus posts y se bloqueará su email y dirección IP." approval: title: "El Post Necesita Aprobación" description: "Hemos recibido tu nuevo post pero necesita ser aprobado por un moderador antes de aparecer. Por favor, ten paciencia." @@ -433,9 +435,7 @@ es: perm_denied_btn: "Permiso denegado" perm_denied_expl: "Has denegado los permisos para las notificaciones en tu navegador web. Configura tu navegador para permitir notificaciones. " disable: "Desactivar notificaciones" - currently_enabled: "(activadas actualmente)" enable: "Activar notificaciones" - currently_disabled: "(desactivadas actualmente)" each_browser_note: "Nota: Tendrás que cambiar esta opción para cada navegador que uses." dismiss_notifications: "Marcador todos como leídos" dismiss_notifications_tooltip: "Marcar todas las notificaciones no leídas como leídas" @@ -470,7 +470,7 @@ es: muted_users: "Silenciados" muted_users_instructions: "Omite todas las notificaciones de estos usuarios." muted_topics_link: "Mostrar temas silenciados" - automatically_unpin_topics: "Quitar destacado automáticamente cuando el usuario llega al final del tema." + automatically_unpin_topics: "Dejar de destacar temas automáticamente cuando los leo por completo." staff_counters: flags_given: "reportes útiles" flagged_posts: "posts reportados" @@ -571,12 +571,26 @@ es: title: "Distintivo de Tarjeta de Usuario" website: "Sitio Web" email_settings: "E-mail" + like_notification_frequency: + title: "Notificar cuando me dan Me gusta" + always: "Con cada Me gusta que reciban mis posts" + first_time_and_daily: "Al primer Me gusta que reciben mis posts y luego diariamente si reciben más" + first_time: "Al primer Me gusta que reciben mi posts" + never: "Nunca" + email_previous_replies: + title: "Incluir respuestas previas al pie de los emails" + unless_emailed: "a menos que se hayan enviado previamente" + always: "siempre" + never: "nunca" email_digests: title: "Cuando no visite la página, enviarme un correo con las últimas novedades." + every_30_minutes: "cada 30 minutos" + every_hour: "cada hora" daily: "diariamente" every_three_days: "cada tres días" weekly: "semanalmente" every_two_weeks: "cada dos semanas" + email_in_reply_to: "Incluir un extracto del post al que se responde en los emails" email_direct: "Envíame un email cuando alguien me cite, responda a mis posts, mencione mi @usuario o me invite a un tema" email_private_messages: "Notifícame por email cuando alguien me envíe un mensaje" email_always: "Quiero recibir notificaciones por email incluso cuando esté de forma activa por el sitio" @@ -703,9 +717,11 @@ es: read_only_mode: enabled: "Este sitio está en modo solo-lectura. Puedes continuar navegando pero algunas acciones como responder o dar \"me gusta\" no están disponibles por ahora." login_disabled: "Iniciar sesión está desactivado mientras el foro esté en modo solo lectura." + logout_disabled: "Cerrar sesión está desactivado mientras el sitio se encuentre en modo de sólo lectura." too_few_topics_and_posts_notice: "¡Vamos a dar por comenzada la comunidad! Hay %{currentTopics} / %{requiredTopics} temas y %{currentPosts} / %{requiredPosts} mensajes. Los nuevos visitantes necesitan algo que leer y a lo que responder." too_few_topics_notice: "¡Vamos a dar por comenzada la comunidad! Hay %{currentTopics} / %{requiredTopics} temas. Los nuevos visitantes necesitan algo que leer y a lo que responder." too_few_posts_notice: "¡Vamos a dar por empezada la comunidad! Hay %{currentPosts} / %{requiredPosts} mensajes. Los nuevos visitantes necesitan algo que leer y a lo que responder." + logs_error_rate_exceeded_notice: "%{timestamp}: La tasa actual de %{rate} errors/%{duration} ha excedido el límite establecido en las opciones del sitio de %{siteSettingLimit} errors/%{duration}." learn_more: "saber más..." year: 'año' year_desc: 'temas creados en los últimos 365 días' @@ -798,6 +814,9 @@ es: twitter: title: "con Twitter" message: "Autenticando con Twitter (asegúrate de desactivar cualquier bloqueador de pop ups)" + instagram: + title: "con Instagram" + message: "Autenticando con Instagram (asegúrate que los bloqueadores de pop up no están activados)" facebook: title: "con Facebook" message: "Autenticando con Facebook (asegúrate de desactivar cualquier bloqueador de pop ups)" @@ -906,6 +925,10 @@ es: posted: "

    {{username}} {{description}}

    " edited: "

    {{username}} {{description}}

    " liked: "

    {{username}} {{description}}

    " + liked_2: "

    {{username}}, {{username2}} {{description}}

    " + liked_many: + one: "

    {{username}}, {{username2}} y otro {{description}}

    " + other: "

    {{username}}, {{username2}} y {{count}} otros {{description}}

    " private_message: "

    {{username}} {{description}}

    " invited_to_private_message: "

    {{username}} {{description}}

    " invited_to_topic: "

    {{username}} {{description}}

    " @@ -1013,8 +1036,8 @@ es: top: "No hay temas en el top más vistos." search: "No hay resultados de búsqueda." educate: - new: '

    Tus nuevos temas aparecerán aquí.

    Por defecto, los temas son considerados nuevos y mostrarán un indicador: nuevo si son creados en los 2 últimos días.

    Puedes cambiar esto en tus preferencias.

    ' - unread: '

    Tus temas sin leer aparecerán aquí.

    Por defecto, los temas son considerados no leídos y mostrán contadores de post sin leer 1 si:

    • Creaste el tema
    • Respondiste al tema
    • Leíste el tema durante más de 4 minutos

    O si has establecido específicamente el tema a Seguir o Vigilar en el control de notificaciones al pie de cada tema.

    Puedes cambiar esto en tus preferencias.

    ' + new: '

    Tus temas nuevos aparecen aquí.

    Por defecto, los temas se consideran nuevos y mostrarán un indicador nuevo si fueron creados en los últimos 2 días.

    Dirígite a preferencias para cambiar esto.

    ' + unread: '

    Tus temas sin leer aparecen aquí.

    Por defecto, los temas son considerados sin leer y mostrarán contadores de posts sin leer 1 si:

    • Creaste el tema
    • Respondiste al tema
    • Leíste el tema por más de 4 minutos

    O si has establecido específicamente el tema como Siguiendo o Vigilando a través del control de notificaciones al pie de cada tema.

    Visita tus preferencias para cambiar esto.

    ' bottom: latest: "No hay más temas recientes para leer." hot: "No hay más temas candentes." @@ -1379,17 +1402,14 @@ es: like: "Deshacer Me gusta" vote: "Deshacer voto" people: - off_topic: "{{icons}} reportó esto como off-topic" - spam: "{{icons}} reportó esto como spam" - spam_with_url: "{{icons}} reportó esto como spam" - inappropriate: "{{icons}} flagged reportó esto como inapropiado" - notify_moderators: "{{icons}} ha notificado a los moderadores" - notify_moderators_with_url: "{{icons}} moderadores notificados" - notify_user: "{{icons}} ha enviado un mensaje" - notify_user_with_url: "{{icons}} ha enviado un mensaje" - bookmark: "{{icons}} ha marcado esto" - like: "{{icons}} les gusta esto" - vote: "{{icons}} ha votado esto" + off_topic: "reportó esto como off-topic" + spam: "reportó esto como spam" + inappropriate: "reportó esto como inapropiado" + notify_moderators: "notificó a moderadores" + notify_user: "envió un mensaje" + bookmark: "guardó esto en marcadores" + like: "le gustó esto" + vote: "votó por esto" by_you: off_topic: "Has reportado esto como off-topic" spam: "Has reportado esto como Spam" @@ -1541,7 +1561,6 @@ es: description: "No serás notificado de ningún tema en estas categorías, y no aparecerán en la página de mensajes recientes." flagging: title: '¡Gracias por ayudar a mantener una comunidad civilizada!' - private_reminder: 'los reportes son privados, son visibles únicamente por los administradores' action: 'Reportar post' take_action: "Tomar medidas" notify_action: 'Mensaje' @@ -1553,7 +1572,7 @@ es: submit_tooltip: "Enviar el reporte privado" take_action_tooltip: "Alcanzar el umbral de reportes inmediatamente, en vez de esperar a más reportes de la comunidad" cant: "Lo sentimos, no puedes reportar este post en este momento." - notify_staff: 'Notificar al Staff' + notify_staff: 'Notificar a los administradores de forma privada' formatted_name: off_topic: "Está fuera de lugar" inappropriate: "Es inapropiado" @@ -1934,11 +1953,11 @@ es: is_disabled: "Restaurar está deshabilitado en la configuración del sitio." label: "Restaurar" title: "Restaurar la copia de seguridad" - confirm: "¿Estás seguro que quieres restaurar esta copia de seguridad?" + confirm: "¿Seguro que quieres restaurar esta copia de seguridad?" rollback: label: "Revertir" title: "Regresar la base de datos al estado funcional anterior" - confirm: "¿Estás seguro que quieres regresar la base de datos al estado funcional anterior?" + confirm: "¿Seguro que quieres retornar la base de datos al estado funcional previo?" export_csv: user_archive_confirm: "¿Seguro que quieres descargar todos tus posts?" success: "Exportación iniciada, se te notificará a través de un mensaje cuando el proceso se haya completado." @@ -2041,9 +2060,6 @@ es: love: name: 'me gusta' description: "El color del botón de \"me gusta\"" - wiki: - name: 'wiki' - description: "Color base usado para el fondo en los posts del wiki." email: title: "Emails" settings: "Ajustes" @@ -2080,6 +2096,20 @@ es: subject: "Asunto" error: "Error" none: "No se encontraron emails entrantes." + modal: + title: "Detalles de emails entrantes" + error: "Error" + return_path: "Ruta de retorno" + message_id: "Id del mensaje" + in_reply_to: "En respuesta a" + references: "Referencias" + date: "Fecha" + from: "De" + to: "Para" + cc: "Cc" + subject: "Asunto" + body: "Cuerpo" + rejection_message: "Correo de rechazo" filters: from_placeholder: "from@example.com" to_placeholder: "to@example.com" @@ -2155,6 +2185,7 @@ es: revoke_admin: "revocar administración" grant_moderation: "conceder moderación" revoke_moderation: "revocar moderación" + backup_operation: "operación de copia de seguridad de respaldo" screened_emails: title: "Correos bloqueados" description: "Cuando alguien trata de crear una cuenta nueva, los siguientes correos serán revisados y el registro será bloqueado, o alguna otra acción será realizada." diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 9d22b193b9..74877eb092 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -179,6 +179,8 @@ fi: more: "Lisää" less: "Vähemmän" never: "ei koskaan" + every_30_minutes: "puolen tunnin välein" + every_hour: "tunnin välein" daily: "päivittäin" weekly: "viikottain" every_two_weeks: "kahden viikon välein" @@ -468,7 +470,6 @@ fi: muted_users: "Vaimennetut" muted_users_instructions: "Älä näytä ilmoituksia näiltä käyttäjiltä" muted_topics_link: "Näytä vaimennetut ketjut" - automatically_unpin_topics: "Poista ketjun kiinnitys automaattisesti, jos selaan keskustelun loppuun" staff_counters: flags_given: "hyödyllistä liputusta" flagged_posts: "liputettuja viestejä" @@ -569,6 +570,12 @@ fi: title: "Käyttäjäkortin tunnus" website: "Nettisivu" email_settings: "Sähköposti" + like_notification_frequency: + title: "Ilmoita, kun viestistäni tykätään" + always: "Aina" + first_time_and_daily: "Ensimmäistä kertaa ja päivittäin" + first_time: "Ensimmäistä kertaa" + never: "Ei koskaan" email_previous_replies: title: "Liitä aiemmat vastaukset mukaan sähköpostin alaosaan" unless_emailed: "ellei aiemmin lähetetty" @@ -576,6 +583,8 @@ fi: never: "ei koskaan" email_digests: title: "Lähetä tiivistelmä uusista viesteistä sähköpostilla, jos en käy sivustolla " + every_30_minutes: "puolen tunnin välein" + every_hour: "tunneittain" daily: "päivittäin" every_three_days: "joka kolmas päivä" weekly: "viikottain" @@ -651,7 +660,7 @@ fi: summary: title: "Yhteenveto" stats: "Tilastot" - topic_count: "Avattuja ketjuja" + topic_count: "Luotuja ketjuja" post_count: "Kirjoitettuja viestejä" likes_given: "Annettuja tykkäyksiä" likes_received: "Saatuja tykkäyksiä" @@ -706,7 +715,8 @@ fi: refresh: "Lataa sivu uudelleen" read_only_mode: enabled: "Sivusto on vain luku -tilassa. Voit jatkaa selailua, mutta vastaaminen, tykkääminen ja muita toimintoja on toistaiseksi poissa käytöstä." - login_disabled: "Kirjautuminen ei ole käytössä sivuston ollessa vain luku -tilassa." + login_disabled: "Et voi kirjautua sisään, kun sivusto on vain luku -tilassa." + logout_disabled: "Et voi kirjautua ulos, kun sivusto on vain luku -tilassa." too_few_topics_and_posts_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentTopics} / %{requiredTopics} ketjua ja %{currentPosts} / %{requiredPosts} viestiä. Uudet kävijät tarvitsevat keskusteluita, joita lukea ja joihin vastata." too_few_topics_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentTopics} / %{requiredTopics} ketjua. Uudet kävijät tarvitsevat keskusteluita, joita lukea ja joihin vastata." too_few_posts_notice: "Laitetaanpa keskustelu alulle! Tällä hetkellä palstalla on %{currentPosts} / %{requiredPosts} viestiä. Uudet kävijät tarvitsevat keskusteluita, joita lukea ja joihin vastata." @@ -1530,7 +1540,6 @@ fi: description: "Et saa ilmoituksia uusista ketjuista näillä alueilla, eivätkä ne näy tuoreimmissa." flagging: title: 'Kiitos avustasi yhteisön hyväksi!' - private_reminder: 'liput ovat yksityisiä, ne näkyvät ainoastaan henkilökunnalle' action: 'Liputa viesti' take_action: "Ryhdy toimiin" notify_action: 'Viesti' @@ -1542,7 +1551,6 @@ fi: submit_tooltip: "Toimita lippu" take_action_tooltip: "Saavuta liputusraja välittömästi, ennemmin kuin odota muidenkin käyttäjien liputuksia." cant: "Pahoittelut, et pysty liputtamaan tätä viestiä tällä hetkellä." - notify_staff: 'Ilmoita ylläpitäjille' formatted_name: off_topic: "Se on asiaankuulumaton" inappropriate: "Se on asiaton" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index ca39f64ba3..643c7fc2f3 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -179,6 +179,8 @@ fr: more: "Plus" less: "Moins" never: "jamais" + every_30_minutes: "toutes les 30 minutes" + every_hour: "chaque heure" daily: "quotidiennes" weekly: "hebdomadaires" every_two_weeks: "bi-mensuelles" @@ -468,7 +470,7 @@ fr: muted_users: "Silencieux" muted_users_instructions: "Cacher toutes les notifications de ces utilisateurs." muted_topics_link: "Afficher les sujets en sourdine" - automatically_unpin_topics: "Automatiquement désépingler les sujets lorsque vous atteignez le bas." + automatically_unpin_topics: "Desépingler automatiquement quand j'arrive à la fin." staff_counters: flags_given: "signalements utiles" flagged_posts: "messages signalés" @@ -569,6 +571,12 @@ fr: title: "Badge pour la carte de l'utilisateur" website: "Site internet" email_settings: "Courriel" + like_notification_frequency: + title: "Notifier lors d'un J'aime" + always: "Toujours" + first_time_and_daily: "La première fois qu'un message est aimé, et quotidiennement" + first_time: "La première fois qu'un message est aimé" + never: "Jamais" email_previous_replies: title: "Inclure les réponses précédentes en bas des courriels" unless_emailed: "sauf si déjà envoyé" @@ -576,6 +584,8 @@ fr: never: "jamais" email_digests: title: "Quand je ne visite pas ce site, m'envoyer un résumé des nouveautés par courriel:" + every_30_minutes: "toutes les 30 minutes" + every_hour: "toutes les heures" daily: "quotidien" every_three_days: "tous les trois jours" weekly: "hebdomadaire" @@ -707,6 +717,7 @@ fr: read_only_mode: enabled: "Le site est en mode lecture seule. Vous pouvez continer à naviguer, mais les réponses, J'aime et autre interactions sont désactivées pour l'instant." login_disabled: "Impossible de se connecté quand le site est en mode lecture seule." + logout_disabled: "Impossible de se deconnecter quand le site est en mode lecture seule." too_few_topics_and_posts_notice: "Démarrons cette discussion! Il y a actuellement %{currentTopics} / %{requiredTopics} sujets et %{currentPosts} / %{requiredPosts} messages. Les nouveaux visiteurs ont besoin de quelques conversations pour lire et répondre." too_few_topics_notice: "Démarrons cette discussion ! Il y a actuellement %{currentTopics} / %{requiredTopics} sujets. Les nouveaux visiteurs ont besoin de quelques conversations à lire et répondre." too_few_posts_notice: "Démarrons cette discussion ! Il y a actuellement %{currentPosts} / %{requiredPosts} messages. Les nouveaux visiteurs ont besoin de quelques conversations à lire et répondre." @@ -802,6 +813,9 @@ fr: twitter: title: "via Twitter" message: "Authentification via Twitter (assurez-vous que les popups ne soient pas bloquées)" + instagram: + title: "avec Instagram" + message: "Authentification via Instagtram (assurez-vous que les popups ne soient pas bloquées)" facebook: title: "via Facebook" message: "Authentification via Facebook (assurez-vous que les popups ne soient pas bloquées)" @@ -910,6 +924,10 @@ fr: posted: "

    {{username}} {{description}}

    " edited: "

    {{username}} {{description}}

    " liked: "

    {{username}} {{description}}

    " + liked_2: "

    {{username}}, {{username2}} {{description}}

    " + liked_many: + one: "

    {{username}}, {{username2}} et 1 autre {{description}}

    " + other: "

    {{username}}, {{username2}} et {{count}} autres {{description}}

    " private_message: "

    {{username}} {{description}}

    " invited_to_private_message: "

    {{username}} {{description}}

    " invited_to_topic: "

    {{username}} {{description}}

    " @@ -1542,7 +1560,6 @@ fr: description: "Vous ne serez jamais notifié de rien concernant les nouveaux sujets dans ces catégories, et elles n'apparaîtront pas dans les dernières catégories." flagging: title: 'Merci de nous aider à garder notre communauté aimable !' - private_reminder: 'les signalements sont privés, seulement visible aux modérateurs' action: 'Signaler ce message' take_action: "Signaler" notify_action: 'Message' @@ -1554,7 +1571,6 @@ fr: submit_tooltip: "Soumettre le signalement privé" take_action_tooltip: "Atteindre le seuil de signalement immédiatement, plutôt que d'attendre plus de signalement de la communauté." cant: "Désolé, vous ne pouvez pas signaler ce message pour le moment" - notify_staff: 'Notifier les responsables' formatted_name: off_topic: "C'est hors-sujet" inappropriate: "C'est inapproprié" @@ -2087,10 +2103,10 @@ fr: references: "References" date: "Date" from: "From" - to: "à" + to: "To" cc: "Cc" - subject: "Objet" - body: "Corps" + subject: "Subject" + body: "Body" filters: from_placeholder: "from@example.com" to_placeholder: "to@example.com" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 76d75644f3..43d207d2c9 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -179,6 +179,8 @@ nl: more: "Meer" less: "Minder" never: "nooit" + every_30_minutes: "elke dertig minuten" + every_hour: "elk uur" daily: "dagelijks" weekly: "wekelijks" every_two_weeks: "elke twee weken" @@ -468,7 +470,7 @@ nl: muted_users: "Negeren" muted_users_instructions: "Negeer alle meldingen van deze leden." muted_topics_link: "Toon gedempte topics." - automatically_unpin_topics: "ontspelt onderwerp automatische wanneer de bodem is bereikt." + automatically_unpin_topics: "Topics automatisch lospinnen als ik het laatste bericht bereik." staff_counters: flags_given: "behulpzame markeringen" flagged_posts: "gemarkeerde berichten" @@ -569,6 +571,11 @@ nl: title: "Badge van gebruikersprofiel" website: "Website" email_settings: "E-mail" + like_notification_frequency: + always: "Altijd" + first_time_and_daily: "De eerste keer dat iemand een bericht leuk vond en dagelijks" + first_time: "De eerste keer dat iemand een bericht leuk vond" + never: "Nooit" email_previous_replies: title: "Voeg de vorige reacties bij onderaan de emails" unless_emailed: "tenzij eerder verzonden" @@ -576,6 +583,8 @@ nl: never: "nooit" email_digests: title: "Stuur me een mail met de laatste updates wanneer ik de site niet bezoek:" + every_30_minutes: "elke dertig minuten" + every_hour: "elk uur" daily: "dagelijks" every_three_days: "elke drie dagen" weekly: "wekelijks" @@ -707,6 +716,7 @@ nl: read_only_mode: enabled: "De site is in alleen lezen modus. Interactie is niet mogelijk." login_disabled: "Zolang de site in read-only modus is, kan er niet ingelogd worden." + logout_disabled: "Uitloggen is uitgeschakeld als de site op alleen lezen staat." too_few_topics_and_posts_notice: "Laten we de discussie starten! Er zijn al %{currentTopics} / %{requiredTopics} topics en %{currentPosts} / %{requiredPosts} berichten. Nieuwe bezoekers hebben conversaties nodig om te lezen en reageren." too_few_topics_notice: "Laten we de discussie starten! Er zijn al %{currentTopics} / %{requiredTopics} topics en %{currentPosts} / %{requiredPosts} berichten. Nieuwe bezoekers hebben conversaties nodig om te lezen en reageren." too_few_posts_notice: "Laten we de discussie starten!. Er zijn al %{currentPosts} / %{requiredPosts} posts Nieuwe bezoekers hebben conversaties nodig om te lezen en reageren." @@ -802,6 +812,9 @@ nl: twitter: title: "met Twitter" message: "Inloggen met een Twitteraccount (zorg ervoor dat je popup blocker uit staat)" + instagram: + title: "met Instagram" + message: "Inloggen met een Instagram-account (zorg ervoor dat je pop-upblocker uitstaat)." facebook: title: "met Facebook" message: "Inloggen met een Facebookaccount (zorg ervoor dat je popup blocker uit staat)" @@ -1542,7 +1555,6 @@ nl: description: "Je zult nooit op de hoogte worden gebracht over nieuwe topics in deze categorie, en ze zullen niet verschijnen in Nieuwste." flagging: title: 'Bedankt voor het helpen beleefd houden van onze gemeenschap!' - private_reminder: 'vlaggen zijn privé, alleen zichtbaar voor de staf' action: 'Meld bericht' take_action: "Onderneem actie" notify_action: 'Bericht' @@ -1554,7 +1566,6 @@ nl: submit_tooltip: "Verstuur de privé markering" take_action_tooltip: "Bereik de vlag drempel direct, in plaats van het wachten op meer gemeenschapsvlaggen" cant: "Sorry, je kan dit bericht momenteel niet melden." - notify_staff: 'Licht de staf in' formatted_name: off_topic: "Het is off topic" inappropriate: "Het is ongepast" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 52ecb1d2eb..27a05d5525 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -218,6 +218,8 @@ ru: more: "Больше" less: "Меньше" never: "никогда" + every_30_minutes: "каждые 30 минут" + every_hour: "каждый час" daily: "ежедневно" weekly: "еженедельно" every_two_weeks: "каждые две недели" @@ -318,7 +320,6 @@ ru: many: "В этой теме {{count}} сообщений, ожидающих проверки" other: "В этой теме {{count}} сообщений, ожидающих проверки" confirm: "Сохранить" - delete_prompt: "Вы уверены, что хотите удалить пользователя %{username}? Это приведет к удалению всех его сообщений, а также заблокирует email и ip адрес." approval: title: "Сообщения для проверки" description: "Ваше сообщение отправлено, но требует проверки и утверждения модератором. Пожалуйста, будьте терпеливы." @@ -474,9 +475,7 @@ ru: perm_default: "Включить оповещения" perm_denied_btn: "Отказано в разрешении" disable: "Отключить оповещения" - currently_enabled: "(сейчас включены)" enable: "Включить оповещения" - currently_disabled: "(сейчас отключены)" each_browser_note: "Примечание: эта настройка устанавливается в каждом браузере индивидуально." dismiss_notifications: "Пометить все прочитанными" dismiss_notifications_tooltip: "Пометить все непрочитанные уведомления прочитанными" @@ -511,7 +510,6 @@ ru: muted_users: "Выключено" muted_users_instructions: "Не отображать уведомления от этих пользователей." muted_topics_link: "Показать темы \"Без уведомлений\"" - automatically_unpin_topics: "Автоматически откреплять топики после прочтения" staff_counters: flags_given: "полезные жалобы" flagged_posts: "сообщения с жалобами" @@ -613,6 +611,8 @@ ru: email_settings: "E-mail" email_digests: title: "В случае моего отсутствия на сайте присылайте мне сводку новостей по почте:" + every_30_minutes: "каждые 30 минут" + every_hour: "каждый час" daily: "ежедневно" every_three_days: "каждые 3 дня" weekly: "еженедельно" @@ -770,8 +770,6 @@ ru: value_prop: "После регистрации мы сможем запоминать, где вы закончили чтение, а когда вы заглянете в ту или иную тему снова, мы откроем ее там, где вы остановились в прошлый раз. Мы также сможем уведомлять вас о новых ответах в любимых темах в вашем личном кабинете или по электронной почте. А самое приятное - после регистрации можно ставить сердечки, тем самым выражая свою симпатию автору. :heartbeat:" summary: enabled_description: "Вы просматриваете выдержку из темы - только самые интересные сообщения по мнению сообщества." - description: "Есть {{replyCount}} ответ(ов)." - description_time: "В теме {{replyCount}} сообщений с ожидаемым временем чтения {{readingTime}} минут." enable: 'Сводка по теме' disable: 'Показать все сообщения' deleted_filter: @@ -1045,9 +1043,6 @@ ru: category: "В разделе {{category}} отсутствуют темы." top: "Нет обсуждаемых тем." search: "Ничего не найдено." - educate: - new: '

    Это список новых тем.

    По-умолчанию, тема считается новой и отображается с индикатором новое, если она была создана в течении последних 2-х дней.

    Это можно изменить в своих настройках.

    ' - unread: '

    Это ваш список непрочитанных тем.

    По-умолчанию, темы считаются непрочитанными и напротив них отображается счетчик непрочитанных сообщений 1, если вы:

    • создали тему;
    • ответили в теме;
    • читали тему более 4-х минут.

    Или же если вы намеренно выбрали Следить или Наблюдать в настройках уведомлений в самом низу темы.

    Эту функциональность можно дополнительно отрегулировать в ваших настройках.

    ' bottom: latest: "Тем больше нет." hot: "Популярных тем больше нет." @@ -1429,18 +1424,6 @@ ru: bookmark: "Удалить из закладок" like: "Больше не нравится" vote: "Отозвать голос" - people: - off_topic: "{{icons}} отметили как оффтопик" - spam: "{{icons}} отметили как спам" - spam_with_url: "{{icons}} пометил это как спам" - inappropriate: "{{icons}} отметили как неуместное" - notify_moderators: "{{icons}} пожаловались модераторам" - notify_moderators_with_url: "{{icons}} пожаловались модераторам" - notify_user: "{{icons}} отправил(и) сообщение" - notify_user_with_url: "{{icons}} отправил(и) сообщение" - bookmark: "{{icons}} добавили в закладки" - like: "Выразили симпатию: {{icons}}" - vote: "{{icons}} проголосовали за" by_you: off_topic: "Помечена вами как оффтопик" spam: "Помечена вами как спам" @@ -1623,7 +1606,6 @@ ru: description: "Не уведомлять о новых темах в этом разделе и скрыть их из последних." flagging: title: 'Спасибо за вашу помощь в поддержании порядка!' - private_reminder: 'жалобы анонимны и видны только персоналу' action: 'Пожаловаться' take_action: "Принять меры" notify_action: 'Сообщение' @@ -1635,7 +1617,6 @@ ru: submit_tooltip: "Отправить приватную отметку" take_action_tooltip: "Достигнуть порога жалоб не дожидаясь большего количества жалоб от сообщества" cant: "Извините, но вы не можете сейчас послать жалобу." - notify_staff: 'Уведомить администрацию' formatted_name: off_topic: "Это не по теме" inappropriate: "Это неприемлемо" @@ -1954,6 +1935,9 @@ ru: automatic_membership_retroactive: "Применить тот же домен электронной почты чтобы добавить существующих зарегистрированных пользователей" default_title: "Заголовок по умолчанию для всех пользователей в группе" primary_group: "Автоматически использовать в качестве главной группы" + group_owners: Владельцы + add_owners: Добавить владельцев + incoming_email_placeholder: "введите email адрес" api: generate_master: "Сгенерировать ключ API" none: "Отсутствует ключ API." @@ -2027,11 +2011,9 @@ ru: is_disabled: "Восстановление отключено в настройках сайта." label: "Восстановить" title: "Восстановить резервную копию" - confirm: "Вы уверенны, что желаете восстановить эту резервную копию?" rollback: label: "Откатить" title: "Откатить базу данных к предыдущему рабочему состоянию" - confirm: "Вы уверены, что хотите откатить базу данных к предыдущему рабочему состоянию?" export_csv: user_archive_confirm: "Вы уверены, то хотите скачать все ваши сообщения?" success: "Процедура экспорта начата, мы отправим вам сообщение, когда процесс будет завершен." @@ -2130,9 +2112,6 @@ ru: love: name: 'любовь' description: "Цвет кнопки «Мне нравится»." - wiki: - name: 'вики' - description: "Базовый цвет, используемый для фона вики-сообщений." email: settings: "Настройки" preview_digest: "Просмотр сводки" diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 1785589903..fc1b5ca2b0 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -199,6 +199,8 @@ sk: more: "Viac" less: "Menej" never: "nikdy" + every_30_minutes: "každých 30 mintút" + every_hour: "každú hodinu" daily: "denne" weekly: "týždenne" every_two_weeks: "každé dva týždne" @@ -294,7 +296,7 @@ sk: few: "Téma má {{count}} príspevky čakajúce na schválenie" other: "Téma má {{count}} príspevkov čakajúcich na schválenie" confirm: "Uložiť zmeny" - delete_prompt: "Ste si istý, že chcete vymazať %{username}? To odstráni všetky ich príspevky a zablokuje ich emailové a IP adresy." + delete_prompt: "Táto akcia zmaže všetky príspevky, zablokuje e-mail a IP adresu užívateľa %{username}. Ste si istý, že chcete zmazať tohto užívateľa? " approval: title: "Príspevok vyžaduje schválenie" description: "Váš príspevok sme obdžali, ale skôr než bude zverejnený musí byť schválený moderátorom. Prosíme o trpezlivosť." @@ -463,9 +465,7 @@ sk: perm_denied_btn: "Prístup zamietnutý" perm_denied_expl: "Povolenie pre zobrazenie notifikácií ste zakázali. Notifikácie povolíte v nastaveniach vášho prehliadača." disable: "Zakázať upozornenia" - currently_enabled: "(momentálne povolené)" enable: "Povoliť upozornenia" - currently_disabled: "(momentálne nepovolené)" each_browser_note: "Poznámka: Toto nastavenie musíte zmeniť v každom používanom prehliadači." dismiss_notifications: "Označiť všetky ako prečítané" dismiss_notifications_tooltip: "Označiť všetky neprečítané upozornenia ako prečítané" @@ -500,7 +500,6 @@ sk: muted_users: "Ignorovaný" muted_users_instructions: "Pozastaviť všetky notifikácie od týchto užívateľov." muted_topics_link: "Zobraziť umlčané témy" - automatically_unpin_topics: "Automaticky zrušiť pripnutie témy ak sa dočítate na koniec." staff_counters: flags_given: "nápomocné značky" flagged_posts: "označkované príspevky" @@ -602,8 +601,17 @@ sk: title: "Odznak karty užívateľa" website: "Webová stránka" email_settings: "Email" + like_notification_frequency: + title: "Oznámiť pri lajknutí" + always: "Vždy" + never: "Nikdy" + email_previous_replies: + always: "vždy" + never: "nikdy" email_digests: title: "Ak to tu nenavštívim, pošlite mi emailový súhrn s novinkami:" + every_30_minutes: "každých 30 mintút" + every_hour: "každú hodinu" daily: "denne" every_three_days: "každé tri dni" weekly: "týždenne" @@ -735,6 +743,7 @@ sk: read_only_mode: enabled: "Stránky sú v móde iba na čítanie. Prosím pokračujte v prezeraní, ale iné akcie, ako odpovedanie, dávanie páči sa mi alebo niektové ďalšie sú teraz vypnuté." login_disabled: "Keď je zapnutý mód iba na čítanie, prihlásenie nie je možné." + logout_disabled: "Odhlásenie nie je možné, kým je stránka v móde iba na čítanie." too_few_topics_and_posts_notice: "Začnime diskusiu! Je tu %{currentTopics} / %{requiredTopics} tém a %{currentPosts} / %{requiredPosts} príspevkov. Noví návštevníci potrebujú mať témy, ktoré môžu čítať a na ktoré budú reagovať." too_few_topics_notice: "Začnime diskusiu! Je tu %{currentTopics} / %{requiredTopics} tém. Noví návštevníci potrebujú mať témy, ktoré môžu čítať a na ktoré budú reagovať." too_few_posts_notice: "Začnime diskusiu! Je tu %{currentPosts} / %{requiredPosts} príspevkov. Noví návštevníci potrebujú mať témy, ktoré môžu čítať a na ktoré budú reagovať." @@ -764,8 +773,6 @@ sk: value_prop: "Keď si vytvoríte účet, zapamätáme si čo ste čítali, takže sa môžete vrátiť presne tam, kde ste prestali. Okrem toho dostanete upozornenie tu, aj na váš e-mail, vždy keď pribudnú nové príspevky. A môžete označiť príspevky ktoré sa vám páčia. :heartbeat:" summary: enabled_description: "Pozeráte sa na zhrnutie tejto témy: najzaujímavejšie príspevky podľa výberu komunity." - description: "Je tu {{replyCount}} odpovedí." - description_time: "Je tu {{replyCount}} odpovedí s priemerným časom čítania {{readingTime}} minút." enable: 'Zhrnutie tejto témy' disable: 'Zobraziť všetky príspevky' deleted_filter: @@ -831,6 +838,9 @@ sk: twitter: title: "pomocou Twitter účtu" message: "Prihlásenie pomocou Twitter účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" + instagram: + title: "so službou Instagram" + message: "Prihlásenie pomocou Instagram účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" facebook: title: "pomocou stránky Facebook" message: "Prihlásenie pomocou Facebook účtu (prosím uistite sa, že vyskakovacie okná sú povolené)" @@ -1048,9 +1058,6 @@ sk: category: "V kategórii {{category}} nie je žiadna téma" top: "Nie sú žiadne populárne témy." search: "Nenašli sa žiadne výsledky" - educate: - new: '

    Tu sa zobrazí Vaša nová téma.

    V predvolenom nastavení sú témy považované za nové a zobrazia sa s príznakom nová pokiaľ boli vytvorené za posledné 2 dni.

    Môžte to zmeniť vo Vašich nastaveniach.

    ' - unread: '

    Tu sa zobrazia Vaše neprečítané témy.

    V predvolenom nastavení sú témy považované za nové a zobrazí sa počet neprečítaných1 Ak ste:

    • Vytvorili tému
    • Odpovedali na tému
    • Čítali tému viac ako 4 minúty

    Alebo ste nastavili na tému Sledovať alebo Pozorovať prostredníctvom ovládania upozornení na konci každej témy.

    Môžte to zmeniť vo Vašich nastaveniach.

    ' bottom: latest: "Nie je už viac najnovšich tém." hot: "Nie je už viac horúcich tém" @@ -1435,18 +1442,6 @@ sk: bookmark: "Vrátiť záložku späť" like: "Zruš \"Páči sa\"" vote: "Zruš hlasovanie" - people: - off_topic: "{{icons}} to označíl ako mimo tému" - spam: "{{icons}} to označíl ako spam" - spam_with_url: "{{icons}} to označíl ako spam" - inappropriate: "{{icons}} to označíl ako nevhodné" - notify_moderators: "{{icons}} upozornil moderátorov" - notify_moderators_with_url: "{{icons}} upozornil moderátorov" - notify_user: "{{icons}} poslal správu" - notify_user_with_url: "{{icons}} poslal správu " - bookmark: "{{icons}} si na to vytvoril záložku" - like: "Páčilo sa to {{icons}}" - vote: "{{icons}} hlasoval za" by_you: off_topic: "Označíli ste to ako mimo tému" spam: "Označíli ste to ako spam" @@ -1615,7 +1610,6 @@ sk: description: "Nikdy nebudete informovaní o udalostiach v nových témach týchto kategórií. Tieto témy sa zároveň nebudú zobrazovať v zozname posledných udalostí." flagging: title: 'Ďakujeme, že pomáhate udržiavať slušnosť v našej komunite!' - private_reminder: 'Označenia sú súkromné viditeľné iba pre personál' action: 'Označ príspevok' take_action: "Vykonať akciu" notify_action: 'Správa' @@ -1627,7 +1621,6 @@ sk: submit_tooltip: "Odoslať súkromné označenie" take_action_tooltip: "Dosiahnuť okamžite limit označení, namiesto čakania na ďalšie označenia od komunity" cant: "Ľutujeme, ale tento príspevok sa teraz nedá označiť ." - notify_staff: 'Notifikovať redakciu' formatted_name: off_topic: "Je to mimo témy" inappropriate: "Je to nevhodné" @@ -2023,11 +2016,9 @@ sk: is_disabled: "Obnovenie je vypnuté na Nastaveniach stránky." label: "Obnoviť" title: "Obnoviť zálohu" - confirm: "Ste si istý, že chcete obnoviť túto zálohu?" rollback: label: "Vrátiť späť" title: "Vrátiť databázu do predchádzajúceho funkčného stavu" - confirm: "Ste si istý, že chcete vrátiť databázu do predchádzajúceho funkčńeho stavu?" export_csv: user_archive_confirm: "Ste si istý, že si chcete stiahnut svoje príspevky?" success: "Export bol spustený, o jeho skončení budete informovaný správou." @@ -2130,9 +2121,6 @@ sk: love: name: 'obľúbené' description: "Farba tlačidla \"Páči sa\"" - wiki: - name: 'wiki' - description: "Základná farba pozadia wiki príspevkov." email: title: "Email" settings: "Nastavenia" @@ -2169,6 +2157,8 @@ sk: subject: "Predmet" error: "Chyba" none: "Nenájdené žiadne ďalšie emaily." + modal: + date: "Dátum" filters: from_placeholder: "from@example.com" to_placeholder: "to@example.com" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 2ac5a03f3b..c970e94ded 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -159,6 +159,8 @@ zh_CN: more: "更多" less: "更少" never: "从未" + every_30_minutes: "每半小时" + every_hour: "每小时" daily: "每天" weekly: "每周" every_two_weeks: "每两周" @@ -438,7 +440,7 @@ zh_CN: muted_users: "忽略" muted_users_instructions: "禁止任何关于这些用户的通知。" muted_topics_link: "显示已忽略的主题" - automatically_unpin_topics: "当你到达底部时自动解除主题置顶。" + automatically_unpin_topics: "当我完整阅读了主题时自动解除置顶。" staff_counters: flags_given: "有效标记" flagged_posts: "被报告的帖子" @@ -487,7 +489,7 @@ zh_CN: upload_title: "上传图片" upload_picture: "上传图片" image_is_not_a_square: "注意:我们已经裁剪了你的图片;它不是正方形的。" - cache_notice: "你已经成功地改变了你的个人头像,但是鉴于浏览器缓存可能要一段时间才能生效。" + cache_notice: "你已经成功地修改了你的个人头像,但是鉴于浏览器缓存可能需要一段时间才会生效。" change_profile_background: title: "个人资料背景" instructions: "个人资料背景将被居中,且默认宽度为 850px。" @@ -538,6 +540,12 @@ zh_CN: title: "用户资料徽章" website: "网站" email_settings: "电子邮箱" + like_notification_frequency: + title: "通知用户赞的消息" + always: "始终" + first_time_and_daily: "每天第一个被赞帖子" + first_time: "第一个被赞的帖子" + never: "从不" email_previous_replies: title: "包括邮件底下的以前的回复" unless_emailed: "除非曾经发送过" @@ -545,6 +553,8 @@ zh_CN: never: "从不" email_digests: title: "当我不访问时,向我的邮箱发送最新信息:" + every_30_minutes: "每半小时" + every_hour: "每小时" daily: "每天" every_three_days: "每三天" weekly: "每周" @@ -675,9 +685,11 @@ zh_CN: read_only_mode: enabled: "这个站点正处于只读模式。请继续浏览,但是回复、赞和其他操作暂时被禁用。" login_disabled: "只读模式下不允许登录。" + logout_disabled: "站点在只读模式下无法登出。" too_few_topics_and_posts_notice: "让我们开始讨论!目前有 %{currentTopics} / %{requiredTopics} 个主题和 %{currentPosts} / %{requiredPosts} 个帖子。新访客需要能够阅读和回复一些讨论。" too_few_topics_notice: "让我们开始讨论!目前有 %{currentTopics} / %{requiredTopics} 个主题。新访客需要能够阅读和回复一些讨论。" too_few_posts_notice: "让我们开始讨论!目前有 %{currentPosts} / %{requiredPosts} 个帖子。新访客需要能够阅读和回复一些讨论。" + logs_error_rate_exceeded_notice: "%{timestamp}:目前的错误率 %{rate}/%{duration}已经超出了站点设置中设置的 %{siteSettingLimit}/%{duration}。" learn_more: "了解更多..." year: '年' year_desc: '365 天以前创建的主题' @@ -769,6 +781,9 @@ zh_CN: twitter: title: "使用 Twitter 帐号登录" message: "正使用 Twitter 帐号验证登录(请确保浏览器没有禁止弹出窗口)" + instagram: + title: "用 Instagram 登录" + message: "使用 Instagram 帐号验证登录(请确保浏览器没有禁止弹出窗口)" facebook: title: "使用 Facebook 帐号登录" message: "正使用 Facebook 帐号验证登录(请确保浏览器没有禁止弹出窗口)" @@ -877,6 +892,9 @@ zh_CN: posted: "

    {{username}} {{description}}

    " edited: "

    {{username}} {{description}}

    " liked: "

    {{username}} {{description}}

    " + liked_2: "

    {{username}}、{{username2}} {{description}}

    " + liked_many: + other: "

    {{username}}、{{username2}}和其他 {{count}} 人{{description}}

    " private_message: "

    {{username}} {{description}}

    " invited_to_private_message: "

    {{username}} {{description}}

    " invited_to_topic: "

    {{username}} {{description}}

    " @@ -1468,7 +1486,6 @@ zh_CN: description: "你不会收到这些分类中的任何新主题通知,并且他们将不会出现在最新列表中。" flagging: title: '感谢帮助社群远离邪恶!' - private_reminder: '标记是不公开的,只有管理人员才可以见到' action: '报告帖子' take_action: "立即执行" notify_action: '消息' @@ -1480,7 +1497,7 @@ zh_CN: submit_tooltip: "提交私有标记" take_action_tooltip: "立即采取标记到达限制值时的措施,而不是等待更多的社群标记" cant: "抱歉,当前你不能报告本帖。" - notify_staff: '通知员工' + notify_staff: '私下通知管理人员' formatted_name: off_topic: "偏题" inappropriate: "不合适" @@ -1578,7 +1595,7 @@ zh_CN: unread: title: "未读" title_with_count: - other: "{{count}} 个未读主题" + other: "未读({{count}})" help: "你正在监视或追踪的主题中有未阅帖子的主题" lower_title_with_count: other: "{{count}} 条未读" @@ -2002,6 +2019,7 @@ zh_CN: cc: "抄送" subject: "主题" body: "内容" + rejection_message: "拒绝邮件" filters: from_placeholder: "from@example.com" to_placeholder: "to@example.com" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index fb8cdfee18..9869b3d646 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -30,6 +30,14 @@ ar: emails: incoming: default_subject: "رسالة ورادة من %{email}" + errors: + inactive_user_error: "يحدث عندما يكون المرسل غير نشط " + blocked_user_error: "يحدث عندما يكون المرسل محظور" + strangers_not_allowed_error: "يحدث عندما يحاول المستخدم انشاء موضوع جديد في قسم هو ليس عضواً فيه" + insufficient_trust_level_error: "يحدث عندما يحاول مستخدم انشاء موضوع جديد في قسم و تكون درجه ثقه المستخدم اقل من القسم المطلوب " + reply_user_not_matching_error: "يحدث عندما ياتي الرد من عنوان بريد ألكتروني مختلف من الذي أرسل اليه ألاشعار " + topic_not_found_error: "يحدث عندما يصل رد لموضوع تم حذفه" + topic_closed_error: "يحدث عندما يصل رد لموضوع تم اغلاقه " errors: &errors format: '%{attribute} %{message}' messages: @@ -201,6 +209,7 @@ ar: rss_description: latest: "آخر المواضيع" hot: "عناوين ساخنة" + top: "أفضل المواضيع" posts: "اخر المشاركات " too_late_to_edit: "أُنشئ المنشور منذ فترة طويلة جدا، لذا تعديله أو حذفه لم يعد ممكنا." excerpt_image: "صورة" @@ -351,6 +360,7 @@ ar: self_parent: "الفئة الفرعية لا يمكن ان تكون كالفئة الرئيسية." depth: "لا يمكنك تضمين فئة فرعية تحت فئة فرعية اخرى" email_in_already_exist: "البريد الإلكتروني الوارد '%{email_in}' مستخدم حاليا للفئة '%{category_name}'." + invalid_email_in: "'%{email_in}' ليس بريداً صالحاَ" cannot_delete: uncategorized: "لايمكن حذف الغير مصنف" has_subcategories: "لايمكن حذف هذه الفئة لأنها تحتوي على فئات فرعية " @@ -362,6 +372,7 @@ ar: many: "لا يمكنك حذف هذه الفئة ﻷن لها مواضيع كثيرة.أقدم موضوع هو %{topic_link}." other: "لا يمكنك حذف هذه الفئة ﻷن لها %{count} مواضيع.أقدم موضوع هو %{topic_link}." topic_exists_no_oldest: "لايمكن حذف هذه الفئة لعدد المواضيع التي تحتويه. عدد المواضيع %{count}." + uncategorized_description: "المواضيع التي لا تحتاج للتصنيف , او لا تتناسق مع اي تصنفيات اخرى موجوده" trust_levels: newuser: title: "مستخدم جديد " @@ -382,6 +393,7 @@ ar: first_day_topics_per_day: "لقد وصلت للعدد الأقصى للمواضيع التي يمكن للعضو الجديد إنشائها في يومهم الأول. رجاء انتظر %{time_left} قبل المحاولة مجددا." create_topic: "أنت تنشأ مواضيع بسرعة عالية. رجاء انتظر %{time_left} قبل المحاولة مجددا." create_post: "أنت ترد بسرعة عالية. رجاء انتظر %{time_left} قبل المحاولة مجددا." + delete_post: "أنت تقوم بمسح المنشورات بسرعه عاليه . الرجاء الانتظار %{time_left} قبل المحاوله مجدداً" topics_per_day: "لقد وصلت للعدد الأقصى للمواضيع الجديدة اليوم. رجاء انتظر %{time_left} قبل المحاولة مجددا." pms_per_day: "لقد وصلت للعدد الأقصى للرسائل اليوم. رجاء انتظر %{time_left} قبل المحاولة مجددا." create_like: "لقد وصلت للعدد الأقصى للإعجابات اليوم. رجاء انتظر %{time_left} قبل المحاولة مجددا." @@ -845,7 +857,6 @@ ar: min_private_message_title_length: "الحد الأدنى المسموح به لطول عنوان لرسالة في الأحرف" min_search_term_length: "الحد الأدنى الصالح لطول مصطلح في الأحرف" allow_uncategorized_topics: "اسمح بتصميم النقاشات من غير فئات. تحذير: اذا كان هناك نقاشات غير مصنفه، يجب عليك تصنيفها قبل اغلاق هذا " - uncategorized_description: "الوصف للفئة غير المصنفة. اتركه فارغا لعدم الوصف." allow_duplicate_topic_titles: "اسمح بالمواضيع المماثلة والعناوين المكررة." unique_posts_mins: "كمية الدقائق التي يمكن للعضو قبلها إنشاء مشاركة مع نفس المحتوى مجددا" educate_until_posts: "عندما يبدا المستخدم كتابه منشورهم الاول.، اظهر انبثاق لوحه تعليمات المستخدم الجديد في المؤلف" @@ -957,6 +968,7 @@ ar: max_username_length: "الحد الأعلى لطول اسم العضو في الأحرف." reserved_usernames: "الأعضاء الغير مسموح لها بالتسجيل." min_password_length: "أقل طول لكلمة المرور" + min_admin_password_length: "أقل طول مسموح به للكلمه مرور بالنسبه لاداري" block_common_passwords: "لا تسمح لكلمات المرور المسجلة في قائمة كلمات المرور الشائعةز" enable_sso: " اسمح تسجيل دخول واحد عبر موقع خارجي(تحذير :عنوان بريد المستخدم *يجب* ان يتم التحقق من صحته عبر الموقع الخارجي)" enable_sso_provider: "تنفيذ مزود بروتوكول Discourse SSO على نقطة نهاية /session/sso_provider, يتطلب sso_secret لتعينها" @@ -977,6 +989,8 @@ ar: enable_twitter_logins: "تفعيل مصادقة تويتر , يطلب : twitter_consumer_key و twitter_consumer_secret" twitter_consumer_key: "مفتاح المستهلك للمصادقة تويتر، مسجلة في http://dev.twitter.com" twitter_consumer_secret: "مفتاح المستهلك للمصادقة تويتر، مسجلة في http://dev.twitter.com" + instagram_consumer_key: "مفتاح الاستخدام لتوثيق الانستغرام " + instagram_consumer_secret: "استخدم توثيق انستغرام السري " enable_facebook_logins: "تمكين مصادقة الفيسبوك، يتطلب facebook_app_id وfacebook_app_secret" facebook_app_id: "التطبيق معرف للمصادقة الفيسبوك، مسجلة في https://developers.facebook.com/apps" facebook_app_secret: "التطبيق معرف للمصادقة الفيسبوك، مسجلة في https://developers.facebook.com/apps" @@ -1023,6 +1037,7 @@ ar: external_system_avatars_enabled: "استخدم خدمات الهه النظام الخارجي " external_system_avatars_url: "عنوان URL لخدمة الصور الرمزية النظام الخارجي. الاستعمال المسموح بها {username} {first_letter} {color} {size}" default_opengraph_image_url: "عنوان رابط صورة ال اوبن جراف الثابتة" + allow_all_attachments_for_group_messages: "اسمح بجميع ملحقات البريد للرسائل الجماعيه " enable_flash_video_onebox: "تمكين التضمين من swf و FLV (أدوبي فلاش) وصلات في مربع واحد. تحذير: قد يعرض المخاطر الأمنية." default_invitee_trust_level: "مستوى الثقة الإفتراضي (0-4) للأعضاء المدعوين." default_trust_level: "مستوى الثقة الإفتراضي (0-4) للأعضاء المدعوين.تحذير! التغيرات ستضعك تحت خطر البريد الغير هام." @@ -1049,6 +1064,7 @@ ar: tl3_links_no_follow: "لا تحذف rel=nofollow من روابط المشاركة بواسطة أعضاء مستوى الثقة 3." min_trust_to_create_topic: "أدنى مستوى ثقة مطلوب لإنشاء موضوع جديد." min_trust_to_edit_wiki_post: "الحد الأدنى لمستوى الثقة المطلوب لتعديل مشاركة معلمة كويكي." + min_trust_to_allow_self_wiki: "الحد الادنى لمستوى ثقه المطلوب لأستحقاق مستخدم مشنور wiki" min_trust_to_send_messages: "المستوي الأدني المطلوب لإرسال رسالة خاصة" newuser_max_links: "عدد الروابط التي يمكن للمستخدم الجديد إضافتها للمشاركة." newuser_max_images: "عدد الصور التي يمكن للمستخدم الجديد إضافتها للمشاركة." @@ -1112,6 +1128,8 @@ ar: strip_images_from_short_emails: "شريط الصور من البريد الإلكتروني لها حجم أقل من 2800 بايت" short_email_length: "طول أقصر بريد الكتروني بـ Bytes." display_name_on_email_from: "أعرض الاسماء كاملة في البريد الالكتروني من المجال." + unsubscribe_via_email: "أسمح للمستخدمين بالغاء الاشتراك لرسائل البريد الالكتروني عن طريق أرسال بريد الكتروني يحتوي على 'unsubscribe' في العنوان او محتوى البريد" + unsubscribe_via_email_footer: "أرفق رابط الغاء الاشتراك الى اخر البريد المرسل " delete_email_logs_after_days: "حذف سجل البريد بعد (ن) يوم. 0 لحفظ السجل للابد" pop3_polling_enabled: "تصويت عبرPOP3 لردود البريد الإلكتروني." pop3_polling_ssl: "أستخدم SSL عند الأتصال بمخدم POP3.(مُستحسن)" @@ -1139,7 +1157,6 @@ ar: automatically_download_gravatars: "تحميل Gravatars للأعضاء عند تغيرهم البريد الإلكتروني الخاص بهم أو عند إنشائهم لحساب آخر." digest_topics: "العدد الاقصي من المواضيع لعرضها في مضمون البريد الإلكتروني " digest_min_excerpt_length: "الحد الادني من مشاركه مقتطفات خلاصه البريد الاكتروني,في الشخصيات" - delete_digest_email_after_days: "احفظ خلاصه الرسائل الالكترونيه للمستخدمين الذين لم تتم رويتهم في الموقع لاكثر من (N) ايام" disable_digest_emails: "عطل ملخص رسائل البريد الإلكتروني لكل الأعضاء." detect_custom_avatars: "سواء او لا تفقد اذا كان المستخدم قام بتحميل صور شخصيه مخصصه" max_daily_gravatar_crawls: "العدد الاقصي من مرات Dicourse سوف يتفقد Gravatar لصور رمزيه مخصصه في اليوم" @@ -1151,6 +1168,7 @@ ar: anonymous_account_duration_minutes: "للحمايه الهويه اصنع حساب مجهول كل n دقائق لكل مستخدم.\nمثال:اذا ضبط 600 , بعد انقضي 600 دقيقه من اخر منشور و غير المستخدم للمجهول.تم انشاء حساب مجهول." hide_user_profiles_from_public: "قم بإلغاء الصفحة الشخصية و حقيبة المستخدم و بطاقة المستخدم للمستخدمين المجهولين" allow_profile_backgrounds: "اسمح للأعضاء برفع خلفيات ملف التعريف." + sequential_replies_threshold: "عدد المشاركات التي يجب على المستخدم انشاءها في عمود في الموضوع الواحد قبل ان يتم تنبيه بكثره الردود المتتاليه ." enable_mobile_theme: "اجهزه الموبايل تستخدم ثيم مناسب للجوال,امكانيه التحويل للموقع الكامل .الغي هذا اذا اردت استخدام انماط سريعه الاستجابه" dominating_topic_minimum_percent: "ما هي النسبه المئويه للمشاركات التي يجب علي المستخدم عملها في الموضوع قبل تلقيه لتنبيه عن خروجه لنطاق الموضوع" disable_avatar_education_message: "عطل الرسالة التعليمية لتغيير الصورة الرمزية." @@ -1160,6 +1178,7 @@ ar: global_notice: "اعرض تنبيه, شعار عام طارئ ملاحظه لكل الزوار, غير الفراغ لاخفاء(HTML مسموح)" disable_edit_notifications: "لتعطيل تحرير الاشعارات بواسطة العضو النظام عندما يكون نشطاً 'download_remote_images_to_local'." automatically_unpin_topics: "تلقائياً قم بنزع الدبوس عندا يصل المستخدم إلي نهاية الصفحه" + read_time_word_count: "عداد الكلمات للدقيقه لحساب الوقت المقدر للقراءه " full_name_required: "الإسم الكامل مطلوب وهو ضروري لإكمال الحساب " enable_names: "عرض الاسم الكامل للعضو , بطاقة العضو , ورسائل البريد الالكتروني , تعطيل عرض الاسم في اي مكان " display_name_on_posts: "عرض الاسم الكامل للعضو على التعليقات بالاضافة الى @username." @@ -1188,7 +1207,7 @@ ar: enable_emoji: "تمكين الرموز التعبيرية " emoji_set: "كيف تريد أن تكون الرموز التعبيرية الخاصة بك؟" enforce_square_emoji: "أجبر نسبة جانب المربع لكل الرموز التعبيرية." - approve_post_count: "كمية مشاركات العضو الجديد التي يجب أن تتم الموافقة عليها" + approve_post_count: "عدد المنشورات للمستخدم الجديد او الاساسي يجب ان تتم الموافقه عليه " approve_unless_trust_level: "مشاركات للأعضاء أدنى من مستوى الثقة هذا يجب أن تتم الموافقة عليها." notify_about_queued_posts_after: "اذا كان هناك مشاركات تنتظر ليتم معينتها لاكثر من هذه الساعات, سوف يتم ارسال بريد الكتروني لبريد الاتصال . اختر 0 لتعطيل هذه الرسائل" default_email_digest_frequency: "عدد المرات التي يتلقى الأعضاء فيها ملخص لبريدهم الإلكتروني إفتراضيا." @@ -1196,6 +1215,7 @@ ar: default_email_direct: "ارسل بريد الكتروني عندما يقوم احدهم بالرد/الاقتباس الي/ذكر او دعوه مستخدم افتراضيا" default_email_mailing_list_mode: "ارسل بريد إلكتروني لكل مشاركة جديدة افتراضيا." default_email_always: "أرسل إشعار بريد إلكتروني حتى عندما يكون العضو متاح إفتراضيا." + default_email_previous_replies: "ارفق الردود السابقه في رسائل البريد افتراضياً" default_other_new_topic_duration_minutes: "الشروط العالمية الافتراضية لموضوع يعتبر جديد." default_other_auto_track_topics_after_msecs: "الوقت العالمي الافتراضي قبل الموضوع متعقب آليا." default_other_external_links_in_new_tab: "أفتح الروابط الخارجية في تبويب جديد إفتراضيا." @@ -1203,6 +1223,7 @@ ar: default_other_dynamic_favicon: "إعرض عدد المواضيع الجديدة/الحديثة في أيقونة المتصفح إفتراضيا." default_other_disable_jump_reply: "لا تتجاوز إلى مشاركة الضو بعد ردهم إفتراضيا." default_other_edit_history_public: "أنشئ مشاركة المراجعات العامة إفتراضيا." + default_other_like_notification_frequency: "أشعر المستخدمين بالاستحسان على المشورات افتراضياً" default_topics_automatic_unpin: "تلقائياً قم إزالة تثبيت المواضيع عندما يصل العضو إلى الأسفل \"إفتراضيا\"." default_categories_watching: "قائمة الفئات التي تشاهد إفتراضيا." default_categories_tracking: "قائمة الفئات التي تتابع إفتراضيا." @@ -1232,7 +1253,6 @@ ar: moved_post: "%{display_username} نقل مشاركتك إلى %{link}" private_message: "%{display_username} أرسل لك رسالة: %{link}" invited_to_private_message: "%{display_username} دعاك لرسالة: %{link}" - invited_to_topic: "%{display_username} دعاك لموضوع: %{link}" invitee_accepted: "%{display_username} قبل دعوتك" linked: "%{display_username} ربطك في %{link}" granted_badge: "كسبت %{link}" @@ -1361,15 +1381,25 @@ ar: characters: "يجب ان تحتوي الارقام والاحرف الصغيرة فقط " unique: "يجب أن يكون فريدا" blank: "يجب أن يكون موجود" - must_begin_with_alphanumeric: "يجب البدء بحرف أو رقم أو _" - must_end_with_alphanumeric: "يجب الإنتهاء بحرف أو رقم أو _" + must_end_with_alphanumeric: "يجب أن ينتهي برقم أو حرف " must_not_contain_two_special_chars_in_seq: "يجب أن لا يحتوي على تسلسل من 2 أو رموز خاصة (.-_)" + must_not_end_with_confusing_suffix: "يجب أن لا ينتهي ب صيغ معقده كــ .json أو .png" email: not_allowed: "بريد الكتروني غير مسموح . يرجى استخدام بريد الكتروني آخر " blocked: "غير مسموح" ip_address: blocked: "لا يُسمح بتسجيل جديد من عنوان ip الخاص بك " max_new_accounts_per_registration_ip: "وفقًا لعنوان IP الخاص بك فقد تم حظر التسجيل (لقد وصلت للحد الأقصى المسموح به) تواصل مع أحد المشرفين." + flags_reminder: + subject_template: + zero: "لا يوجد تبليغات تنتظر التعامل معها" + one: "تبليغ 1 ينتظر التعامل معها" + two: "تبليغان ينتظران التعامل معها" + few: "%{count} تبليغات تنتظر التعامل معها" + many: "%{count} تبليغات تنتظر التعامل معها" + other: "%{count} تبليغات تنتظر التعامل معها" + unsubscribe_mailer: + subject_template: "اكد انك لا تريد استقبال بريد الكتروني من %{site_title} بعد ألان" invite_mailer: subject_template: "%{invitee_name} دعاك إلى '%{topic_title}' على %{site_domain_name}" text_body_template: | @@ -1413,95 +1443,10 @@ ar: (إذا انتهت مدة صلاحية الرابط أعلاه، اختر "نسيت كلمة المرور" عند تسجيل الدخول مع عنوان البريد الإلكتروني الخاص بك.) test_mailer: subject_template: "[%{site_name}] بريد الكتروني بهدف الاختبار" - text_body_template: | - هذا بريد إختبار إلكتروني من - - [**%{base_url}**][0] - - أهداف البريد معقدة. هنا بعض الأشياء المهمة يجب عليك مراجعتها أولا: - - - كن *متأكد* لوضع `إشعارات البريد الإلكتروني` من: عنوان صحيح في إعدادات موقعك. **النطاق المخصص في أو "من" عنوان رسائل البريد الإلكتروني التي أرسلتها سيتم التحقق تجاه مجال بريدك الإلكتروني**. - - - معرفة كيفية عرض المصدر الخام للبريد الإلكتروني في عميل بريدك , لذا يمكنك البحث في رؤوس صفحات البريد الإلكتروني للأدلة المهمة. في Gmail, خيار "إظهار الأصلية" في القائمة المنسدلة في أعلى اليمين لكل بريد إلكتروني. - - - **مهم جدا:** هل الـ ISP الخاص بك له سجل DNS عكسي مدخل لربط أسماء النطاقات وعناوين IP التي أرسلت بريد منها؟ [اختبر سجل PTR العكسي][2] هنا. إذا كان ISP الخاص بك لم يدخل سجل مؤشر DNS العكسي المناسب, من غير المحتمل جدا تسليم أي من بريدك الإلكتروني. - - - هل مجالك لـ[سجل SPF][8] صحيح؟ [أختبر سجل SPF الخاص بك][1] هنا. لاحظ أن TXT هو نوع سجل رسمي صحيح لـSPF. - - - Iهل مجالك لـ [DKIM سجل][3] صحيح؟ هذا سيحسن بشكل كبير أهداف البريد الإلكتروني. [أختبر سجل DKIM الخاص بك][7] هنا. - - - إذا شغلت خادم البريد الخاص بك, راجع للتأكد من IPs لخادم بريدك [ليست في أي قوائم سوداء][4]. أيضا تحقق من إرسال تأكيد مضيف كامل التأهيل الذي يحل في DNS في رسالته الترحيبية. إذا كان لا, سيسبب رفض بريدك الإلكتروني من عدة خدمات. - - (الطريقة *السهلة* هي إنشاء حساب مجاني في [Mandrill][md] أو [Mailgun][mg] أو [Mailjet][mj], التي لديها عطاء مجاني والبريد مجاني والخطط وستكون جيدة للإتصالات القصيرة. لا تزال بحاجة لإعداد سجلات SPF و DKIM في DNS الخاص بك, رغم ذلك!) - - نأمل منك أن يلقي إختبار أهداف البريد الإلكتروني الموافقة! - - حظ جيد, - - أصدقائك في [Discourse](http://www.discourse.org) - - [0]: %{base_url} - [1]: http://www.kitterman.com/spf/validate.html - [2]: http://mxtoolbox.com/ReverseLookup.aspx - [3]: http://www.dkim.org/ - [4]: http://whatismyipaddress.com/blacklist-check - [7]: http://dkimcore.org/tools/dkimrecordcheck.html - [8]: http://www.openspf.org/SPF_Record_Syntax - [md]: http://mandrill.com - [mg]: http://www.mailgun.com/ - [mj]: https://www.mailjet.com/pricing new_version_mailer: subject_template: "[%{site_name}] يوجد اصدار جديد , تحديث متوفر" - text_body_template: | - نسخة جديدة من [Discourse](http://www.discourse.org) متوفرة. - - نسختك: %{installed_version} - النسخة الجديدة: **%{new_version}** - - ربما تريد أن: - - - ما الجدي في [GitHub changelog](https://github.com/discourse/discourse/commits/master). - - - التحديث من متصفحك من [%{base_url}/admin/upgrade](%{base_url}/admin/upgrade). - - - زيارة [meta.discourse.org](http://meta.discourse.org) للأخبار, والمناقشة, ودعم Discourse. new_version_mailer_with_notes: subject_template: "[%{site_name}] التحديث متوفر" - text_body_template: | - نسخة جديدة من [Discourse](http://www.discourse.org) متوفرة. - - نسختك: %{installed_version} - النسخة الجديدة: **%{new_version}** - - ربما تريد أن: - - - ما الجدي في [GitHub changelog](https://github.com/discourse/discourse/commits/master). - - - التحديث من متصفحك من [%{base_url}/admin/upgrade](%{base_url}/admin/upgrade). - - - زيارة [meta.discourse.org](http://meta.discourse.org) للأخبار, والمناقشة, ودعم Discourse. - - ### ملاحظات الإصدارة - - %{notes} - flags_reminder: - flags_were_submitted: - zero: "لم ترسل تبليغات خلال الساعات الماضية." - one: "هذه التبليغات أرسلت خلال ساعة مضت." - two: "هذه التبليغات أرسلت خلال ساعتان مضتا." - few: "هذه التبليغات أرسلت خلال %{count} ساعات مضت." - many: "هذه التبليغات أرسلت خلال %{count} ساعات مضت." - other: "هذه التبليغات أرسلت خلال %{count} ساعات مضت." - please_review: "يرجى مراجعة ذلك " - post_number: "مشاركة" - how_to_disable: 'يمكنك تعطيل أو تغيير تكرار تذكير البريد الإلكتروني عبر "إشعار حول الإعلامات بعد" الإعدادات.' - subject_template: - zero: "لا يوجد تبليغات تنتظر التعامل معها" - one: "تبليغ 1 ينتظر التعامل معها" - two: "تبليغان ينتظران التعامل معها" - few: "%{count} تبليغات تنتظر التعامل معها" - many: "%{count} تبليغات تنتظر التعامل معها" - other: "%{count} تبليغات تنتظر التعامل معها" queued_posts_reminder: subject_template: zero: "[%{site_name}] %{count} المشاركات التي تنتظر معينتها" @@ -1622,7 +1567,6 @@ ar: text_body_template: "نحن آسفون، لكنه فشل تصدير البيانات الخاصة بك. يرجى التحقق من السجلات أو اتصل بأحد المشرفين." email_reject_no_account: subject_template: "[%{site_name}] بريد الكتروني -- حساب غير معروف" - text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nليس هنالك حساب عضو يمتلك هذا البريد الالكتروني. حاول أن ترسل من بريد الكتروني مختلف، أو أتصل بـ أحد المشرفين.\n\n" email_reject_empty: subject_template: "[%{site_name}] بريد الكتروني -- بدون محتوى" text_body_template: |2 @@ -1637,24 +1581,23 @@ ar: email_reject_invalid_access: subject_template: "[%{site_name}] بريد الكتروني -- غير صالح" text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nحسابك لا يمتلك الصلاحيات لنشر مواضيع جديدة في تلك الفئة. إذا كنت تعتقد أن هذا الخطأ، اتصل بالاعضاء المشرفين.\n" + email_reject_strangers_not_allowed: + subject_template: "مشاكل البريد ألالكتروني [%{site_name}] -- دخول غير صالح" + email_reject_invalid_post: + subject_template: "مشاكل البريد ألالكتروني [%{site_name}] -- مشكله في النشر " + email_reject_invalid_post_specified: + subject_template: "مشاكل البريد ألالكتروني [%{site_name}] -- مشكله في النشر " email_reject_reply_key: subject_template: "[%{site_name}] بريد الكتروني -- مفتاح رد غير معروف" - text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nمفتاح الرد المُقدم غير صالح أو غير معروف، لذلك نحن لا نعرف ما هو هذا البريد الإلكتروني للرد عليه. أتصل بالأعضاء المشرفين.\n" email_reject_topic_not_found: subject_template: "[%{site_name}] بريد الكتروني -- موضوع غير موجود" - text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nالموضوع الذي تريد الرد عليه لم يعد موجودا، وربما تم حذفه؟ إذا كنت تعتقد أن هذا خطأ، اتصل أحد الاعضاء المشرفين. \n" email_reject_topic_closed: subject_template: "[%{site_name}] بريد الكتروني -- موضوع مغلق" text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nالموضوع الذي تريد الرد عليه مغلق حاليا والتي لم تعد تقبل ردود. إذا كنت تعتقد أن هذا خطأ، اتصل أحد الأعضاء المشرفين.\n" email_reject_auto_generated: subject_template: "[%{site_name}] بريد الكتروني -- توليد رد آلي" - text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\n بريدك الإلكتروني عُلّم كـ\"مولد تلقائي\"، وهو ما لا يمكن قبوله. إذا كنت تعتقد أن هذا خطأ، اتصل أحد الاعضاء المشرفين.\n" email_error_notification: subject_template: "[%{site_name}] بريد الكتروني -- خطأ مصادقة POP" - text_body_template: | - خطأ في المصادقة عند استطلاع الرسائل من خادم POP . - - من فضلك تأكد من تكوين إعدادات خادم POP من[إعدادات الموقع](%{base_url}/admin/site_settings/category/email). too_many_spam_flags: subject_template: "حساب جديد مقفول" text_body_template: | @@ -1736,7 +1679,6 @@ ar: يرجى زيارة هذا الرابط لعرض الرسالة: %{base_url}%{url} user_invited_to_topic: - subject_template: "[%{site_name}] %{username} دعوتك للموضوع '%{topic_title}'" text_body_template: |2 %{username} دعوتك للنقاش @@ -1874,7 +1816,7 @@ ar: انقر على الرابط التالي لاختيار كلمة مرور لحسابك الجديد: %{base_url}/users/password-reset/%{email_token} - confirm_new_email: + authorize_email: subject_template: "[%{site_name}] تأكيد البريد الإلكتروني الجديد " text_body_template: | قم بتاكيد عنوان بريدك الالكتروني لـ %{site_name} عن طريق الضغط علي الرابط التالي: @@ -1906,7 +1848,6 @@ ar: إذا كان الرابط أعلاه غير قابل للنقر، حاول نسخه ولصقه في شريط العناوين في متصفح الويب عندك. page_not_found: - title: "الصفحة المطلوبة غير موجودة او ليس لديك صلاحيات لرؤيتها " popular_topics: "شعبي " recent_topics: "الأخيرة" see_more: "المزيد" diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index ae2c4febeb..bc7da6f949 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -30,6 +30,19 @@ fi: emails: incoming: default_subject: "Uusi sähköposti osoitteesta %{email}" + 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)." + auto_generated_email_error: "Näin käy, kun viestin kiireellisyysluokitus (engl. precedence header) on joku seuraavista: list, junk, bulk tai auto_reply, tai kun joku muu otsikkotiedoista sisältää jonkun seuraavista: auto-submitted, auto-replied tai auto-generated." + no_body_detected_error: "Näin käy, kun leipätekstin poiminta epäonnistuu eikä liitteitä ole." + inactive_user_error: "Näin käy, kun lähettäjä ei ole aktiivinen." + blocked_user_error: "Näin käy, kun lähettäjä on estetty." + bad_destination_address: "Näin käy, kun viestin vastaanottaja/kopio/piilokopio -kenttien osoitteet eivät täsmää asetettuihin saapuvan sähköpostin osoitteiden kanssa." + strangers_not_allowed_error: "Näin käy, kun käyttäjä yrittää luoda ketjun alueelle, jonka jäsen ei ole." + insufficient_trust_level_error: "Näin käy, kun käyttäjä yrittää luoda ketjun alueelle, jonka vähimmäisluottamustasovaatimusta ei täytä." + reply_user_not_matching_error: "Näin käy, kun vastaus saapuu eri sähköpostiosoitteesta kuin mihin ilmoitus lähetettiin." + topic_not_found_error: "Näin käy, kun vastauksen saapuessa ketju, johon viesti oli tarkoitettu, on poistettu." + topic_closed_error: "Näin käy, kun vastauksen saapuessa ketju, johon viesti oli tarkoitettu, on suljettu." errors: &errors format: '%{attribute} %{message}' messages: @@ -304,6 +317,7 @@ fi: self_parent: "Alue ei voi olla itsensä ylempi alue." depth: "Alue ei voi olla tytäralueen tytäralue" email_in_already_exist: "Sisääntulevan sähköpostin osoite '%{email_in}' on jo käytössä alueella '%{category_name}'." + invalid_email_in: "'%{email_in}' ei ole käypä sähköpostiosoite." cannot_delete: uncategorized: "Alueettomat-aluetta ei voi poistaa" has_subcategories: "Aluetta ei voi poistaa, koska sillä on sisempiä alueita." @@ -326,15 +340,16 @@ fi: change_failed_explanation: "Yritit alentaa käyttäjän %{user_name} luottamustasolle '%{new_trust_level}'. Käyttäjän luottamustaso on jo valmiiksi '%{current_trust_level}'. %{user_name} jatkaa edelleen luottamustasolla '%{current_trust_level}' - jos haluat alentaa luottamustasoa, lukitse se ensin" rate_limiter: slow_down: "Olet tehnyt tämän toiminnon liian monta kertaa, yritä uudelleen myöhemmin." - too_many_requests: "Sille, kuinka useasti tämä toiminto voidaan suorittaa on määritelty päivittäinen raja. Odota %{time_left} ennen uudelleen kokeilemista." + too_many_requests: "Tämä toiminto voidaan suorittaa vain määrätyn monta kertaa päivässä. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." by_type: - first_day_replies_per_day: "Olet lähettänyt enimmäismäärän viestejä, jonka uusi käyttäjä saa lähettää ensimmäisenä päivänään. Odota %{time_left}, ennen kuin yrität uudelleen." - first_day_topics_per_day: "Olet luonut enimmäismäärän ketjuja, jonka uusi käyttäjä saa lähettää ensimmäisenä päivänään. Odota %{time_left}, ennen kuin yrität uudelleen." - create_topic: "Yrität luoda ketjuja liian nopeasti. Odota %{time_left}, ennen kuin yrität uudelleen." - create_post: "Yrität vastata liian nopeasti. Odota %{time_left}, ennen kuin yrität uudelleen." - topics_per_day: "Olet luonut enimmäismäärän uusia ketjuja tälle päivälle. Odota %{time_left}, ennen kuin yrität uudelleen." - pms_per_day: "Olet lähettänyt enimmäismäärän viestejä tälle päivälle. Odota %{time_left}, ennen kuin yrität uudelleen." - create_like: "Olet tykännyt enimmäismäärän tälle päivälle. Odota %{time_left}, ennen kuin yrität uudelleen." + first_day_replies_per_day: "Uusi käyttäjä ei voi ensimmäisenä päivänään kirjoittaa enempää viestejä. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." + first_day_topics_per_day: "Uusi käyttäjä ei voi ensimmäisenä päivänään luoda enempää ketjuja. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." + create_topic: "Yrität luoda ketjuja liian nopeasti. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." + create_post: "Yrität vastata liian nopeasti. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." + delete_post: "Poistat viestejä liian nopeasti. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." + topics_per_day: "Olet luonut enimmäismäärän uusia ketjuja tälle päivälle. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." + pms_per_day: "Olet lähettänyt enimmäismäärän viestejä tälle päivälle. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." + create_like: "Olet tykännyt enimmäismäärän tälle päivälle. Ole hyvä ja odota %{time_left} ja yritä sitten uudelleen." create_bookmark: "Olet lisännyt enimmäismäärän kirjanmerkkejä tälle päivälle. Odota %{time_left}, ennen kuin yrität uudelleen." edit_post: "Olet saavuttanut muokkausten enimmäismäärän tälle päivälle. Odota %{time_left}, ennen kuin yrität uudelleen." hours: @@ -806,6 +821,7 @@ fi: max_username_length: "Käyttäjänimen enimmäispituus merkeissä." reserved_usernames: "Käyttäjänimet, joiden rekisteröintiä ei sallita." min_password_length: "Salasanan vähimmäispituus." + min_admin_password_length: "Ylläpitäjän salasanan vähimmäispituus." block_common_passwords: "Älä salli salasanoja, jotka ovat 10 000 yleisimmän salasanan joukossa." enable_sso: "Ota käyttöön single sign on ulkopuolisen sivuston kautta (VAROITUS: ULKOPUOLISEN SIVUSTON TÄYTYY VALIDOIDA SÄHKÖPOSTIOSOITTEET!)" enable_sso_provider: "Ota käyttöön Discourse SSO provider -protokolla /session/sso_provider päätepisteessä, vaatii asetuksen sso_secret asettamista." @@ -838,6 +854,7 @@ fi: backup_frequency: "Kuinka usein luodaan sivuston varmuuskopio, päivissä." enable_s3_backups: "Lataa varmuuskopiot S3:een niiden valmistuttua. TÄRKEÄÄ: edellyttää, että toimivat S3 kirjautumistiedot on syötetty asetuksiin." s3_backup_bucket: "Amazon S3 bucket johon varmuuskopiot ladataan. VAROITUS: Varmista, että se on yksityinen." + s3_disable_cleanup: "Älä poista varmuuskopiota S3:sta, kun se poistetaan paikallisesti." backup_time_of_day: "UTC-kellonaika, jolloin varmuuskopio tehdään." backup_with_uploads: "Sisällytä lataukset ajastettuihin varmuuskopioihin. Jos tämä on pois käytöstä, vain tietokanta varmuuskopioidaan." active_user_rate_limit_secs: "Kuinka usein 'last_seen_at' kenttä päivitetään, sekunneissa" @@ -854,7 +871,7 @@ fi: max_flags_per_day: "Liputusten päivittäinen maksimäärä per käyttäjä." max_bookmarks_per_day: "Kirjanmerkkien päivittäinen maksimäärä per käyttäjä." max_edits_per_day: "Muokkausten päivittäinen maksimäärä per käyttäjä." - max_topics_per_day: "Luotujen ketjujen päivittäinen maksimäärä per käyttäjä." + max_topics_per_day: "Luotujen ketjujen päivittäinen enimmäismäärä per käyttäjä." max_private_messages_per_day: "Viestien päivittäinen maksimäärä per käyttäjä" max_invites_per_day: "Maksimimäärä kutsuja, jonka käyttäjä voi lähettää päivässä." max_topic_invitations_per_day: "Maksimimäärä ketjukutsuja, jonka yksittäinen käyttäjä voi lähettää päivässä" @@ -1048,7 +1065,6 @@ fi: enable_emoji: "Ota emoji käyttöön" emoji_set: "Minkälaisa emojia haluaisit käyttää?" enforce_square_emoji: "Pakota neliö kuvasuhteeksi kaikille emojille." - approve_post_count: "Viestien lukumäärä, joka tarkastetaan uusilta käyttäjiltä" approve_unless_trust_level: "Tätä luottamustasoa alhaisempien käyttäjien viestit tarkastetaan" notify_about_queued_posts_after: "Jos hyväksyntää odottavia viestejä on odottanut näin monta tuntia, lähetä sähköposti contact_email osoitteeseen. Aseta 0 ottaaksesi pois käytöstä." default_email_digest_frequency: "Kuinka usein käyttäjille lähetetään tiivistelmäsähköposti oletuksena." @@ -1829,7 +1845,7 @@ fi: Klikkaa seuraavaa linkkiä asettaaksesi salasanan uudelle tunnuksellesi: %{base_url}/users/password-reset/%{email_token} - confirm_new_email: + authorize_email: subject_template: "[%{site_name}] Vahvista uusi sähköpostiosoite" text_body_template: | Vahvista uusi sähköpostiosoite sivustolle %{site_name} klikkaamalla alla olevaa linkkiä: diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 513fa6abf4..812ecaee9c 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -12,7 +12,7 @@ nl: long_date: "D MMMM YYYY H:mm" datetime_formats: &datetime_formats formats: - short: "%m-%d-%Y" + short: "%d-%m-%Y" short_no_year: "%-d %B" date_only: "%B %-d, %Y" date: @@ -39,7 +39,7 @@ nl: blocked_user_error: "Gebeurt wanneer de zender is geblokkeerd." bad_destination_address: "Gebeurt wanneer geen van de E-mail adressen in de To/Cc/Bcc velden gelijk zijn aan de geconfigureerde inkomende E-mail adressen." strangers_not_allowed_error: "Gebeurt wanneer een gebruiker heeft geprobeerd een nieuw topic aan te maken in een categorie waar zijn geen lid van zijn." - insufficient_trust_level_error: "Gebeurt wanneer een gebruiker heeft geprobeerd een nieuw topic aan te maken in een categorie waar zijn niet het vereiste trust level voor hebben." + insufficient_trust_level_error: "Gebeurt als een gebruiker heeft geprobeerd om een nieuw topic aan te maken in een categorie waarvoor hij niet het benodigde vertrouwensniveau heeft." reply_user_not_matching_error: "Gebeurt wanneer een reactie kwam van een ander E-mail adres dan waar de notificatie was heen gestuurd. " topic_not_found_error: "Gebeurt wanneer een reactie binnen kwam, maar het topic al is verwijdert." topic_closed_error: "Gebeurt wanneer een reactie binnen kwam, maar het verwante topic is gesloten." @@ -340,11 +340,15 @@ nl: first_day_topics_per_day: "Je hebt het maximum aantal topics dat een nieuwe gebruiker op hun eerste dag kan creëren bereikt. Wacht %{time_left} voordat je het opnieuw probeert." create_topic: "Je maakt te snel topics aan. Wacht %{time_left} voordat je het opnieuw probeert. " create_post: "Je reageert te snel. Wacht %{time_left} voordat je het opnieuw probeert. " + delete_post: "Je verwijdert berichten te snel. Wacht %{time_left} voor je het opnieuw probeert." topics_per_day: "Je hebt het maximum aantal nieuwe topics bereikt voor vandaag. Wacht %{time_left} voordat je het opnieuw probeert. " pms_per_day: "Je hebt het maximum aantal berichten bereikt voor vandaag. Wacht a.u.b. %{time_left} voordat je het opnieuw probeert." create_like: "Je hebt het maximum aantal likes voor vandaag bereikt. Wacht aub %{time_left} voordat je het opnieuw probeert." create_bookmark: "Je hebt het maximum aantal bladwijzers voor vandaag bereikt. Wacht aub %{time_left} voordat je het opnieuw probeert." edit_post: "Je hebt het maximum aantal bewerkingen voor vandaag bereikt. Wacht aub %{time_left} voordat je het opnieuw probeert." + live_post_counts: "Je vraagt te snel om het huidige aantal berichten. Wacht %{time_left} voor je het opnieuw probeert." + unsubscribe_via_email: "Je hebt het maximum aantal uitschrijvingen voor vandaag bereikt. Wacht aub %{time_left} voordat je het opnieuw probeert." + topic_invitations_per_day: "Je hebt het maximum aantal topic uitnodigingen bereikt voor vandaag. Wacht aub %{time_left} voordat je het opnieuw probeert." hours: one: "1 uur" other: "%{count} uren" @@ -497,6 +501,7 @@ nl: long_form: 'markeerde dit als ongepast' notify_moderators: title: "Iets anders" + description: 'Dit topic moet door een staflid bekeken worden. Dit vanwege de regels, voorwaarden of vanwege een andere reden.' long_form: 'heeft dit gemarkeerd voor moderatie' email_title: 'De topic "%{title}" moet door een moderator worden bekeken' email_body: "%{link}\n\n%{message}" @@ -714,6 +719,7 @@ nl: download_remote_images_to_local: "Download externe afbeeldingen en sla ze lokaal op. Dit voorkomt dat afbeeldingen niet meer beschikbaar zouden kunnen worden." download_remote_images_threshold: "Minimum schijfruimte vereist om externe afbeeldingen lokaal te downloaden (percentage)" disabled_image_download_domains: "Externe afbeeldingen zullen nooit gedownload worden van deze domeinen. Pipe-gescheiden lijst." + editing_grace_period: "De eerste (n) seconden na het plaatsen van een bericht leidt het wijzigen ervan niet tot een nieuwe versie in de geschiedenis van het bericht." post_edit_time_limit: "Berichten mogen na (n) minuten nog gewijzigd of verwijderd worden door de schrijver. Gebruik 0 voor onbeperkt." edit_history_visible_to_public: "Sta toe dat iedereen vorige versies van een gewijzigd bericht kan zien. Als dit uit staat kunnen alleen stafleden dit zien." delete_removed_posts_after: "Berichten verwijderd door de auteur zullen automatisch verwijderd worden na (n) uur. Als dit ingesteld is op 0 zullen berichten direct verwijderd worden." @@ -765,6 +771,7 @@ nl: allow_html_tables: "Sta toe dat tabellen in Markdown mogen worden ingevoerd met behulp van HTML-tags. TABLE, TD, TR, TH zullen aan de whitelist worden toegevoegd (vereist volledig herbouwen van alle oude berichten met tabellen)" post_undo_action_window_mins: "Het aantal minuten waarin gebruikers hun recente acties op een bericht nog terug kunnen draaien (liken, markeren, etc)." must_approve_users: "Stafleden moeten alle nieuwe gebruikersaccounts goedkeuren voordat ze de site mogen bezoeken. OPGELET: als dit wordt aangezet voor een actieve site wordt alle toegang voor bestaande niet stafleden ingetrokken." + pending_users_reminder_delay: "Stuur moderators een bericht als nieuwe gebruikers al langer dan dit aantal uren op goedkeuring wacht. Stel in op -1 om notificaties uit te schakelen." ga_tracking_code: "Google analytics (ga.js) trackingcode, bijv. UA-12345678-9; zie: http://google.com/analytics" ga_domain_name: "Google analytics (ga.js) domeinnaam, bijv. mijnsite.nl; zie http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) trackingcode, bijv. UA-12345678-9; zie http://google.com/analytics" @@ -811,6 +818,7 @@ nl: max_username_length: "Maximale lengte gebruikersnaam in karakters." reserved_usernames: "Gebruikersnamen waarvoor registratie niet is toegestaan." min_password_length: "Minimum lengte wachtwoord." + min_admin_password_length: "Minimale wachtwoordlengte voor de beheerder." block_common_passwords: "Accepteer geen wachtwoorden die voorkomen in de 10.000 meest voorkomende wachtwoorden." enable_sso: "Schakel single sign on in via een externe site (WAARSCHUWING: E-MAILADRESSEN VAN GEBRUIKERS '* MOETEN * DOOR DE EXTERNE SITE WORDEN GEVALIDEERD)" enable_sso_provider: "Pas Discourse SSO provider protocol toe op het /session/sso_provider eindpunt, vereist dat sso_secret is ingesteld" @@ -831,6 +839,7 @@ nl: enable_twitter_logins: "Zet inloggen met Twitter aan. Hiervoor heb je een twitter_consumer_key en twitter_consumer_secret nodig." twitter_consumer_key: "consumer_key (registreer op dev.twitter.com)" twitter_consumer_secret: "consumer secret (registreer op dev.twitter.com)" + enable_instagram_logins: "Zet inloggen met Instagram aan. Hiervoor heb je een instagram_consumer_key and instagram_consumer_secret nodig" enable_facebook_logins: "Zet inloggen met Facebook aan. Hiervoor heb je een facebook_app_id en facebook_app_secret nodig." facebook_app_id: "app_id (registreer op https://developers.facebook.com/apps)" facebook_app_secret: "app_secret (registreer op https://developers.facebook.com/apps)" @@ -860,6 +869,8 @@ nl: max_private_messages_per_day: "Het maximum aantal berichten dat een gebruiker per dag kan maken." max_invites_per_day: "Het maximum aantal uitnodigingen dat een gebruiker per dag kan versturen." max_topic_invitations_per_day: "Het maximum aantal uitnodigingen voor een topic dat een gebruiker per dag kan sturen." + alert_admins_if_errors_per_minute: "Aantal foutmeldingen per minuut waarbij de beheerder een melding krijgt. Een waarde van 0 schakelt deze mogelijkheid uit. LET OP: herstart vereist." + alert_admins_if_errors_per_hour: "Aantal foutmeldingen per uur waarbij de beheerder een melding krijgt. Een waarde van 0 schakelt deze mogelijkheid uit. LET OP: herstart vereist." suggested_topics: "Aantal aanbevolen topics onderaan een topic." limit_suggested_to_category: "Alleen topics van de huidige categorie weergeven in aanbevolen topics." clean_up_uploads: "Verwijder weesbestanden zonder referentie om illegale hosting te voorkomen. LET OP: maak een back-up van je /uploads directory voordat je deze instelling aanzet." @@ -890,6 +901,8 @@ nl: tl2_requires_likes_received: "Hoeveel likes een lid moet ontvangen voordat deze naar trustlevel 2 gepromoveerd wordt." tl2_requires_likes_given: "Hoeveel likes een lid moet geven voordat deze naar trustlevel 2 gepromoveerd wordt." tl2_requires_topic_reply_count: "Op hoeveel topics een lid moet reageren voordat deze naar trustlevel 2 gepromoveerd wordt." + tl3_time_period: "Benodigde periode voor Vertrouwensniveau 3" + tl3_requires_days_visited: "Het minimum aantal dagen waarop een gebruiker de site in de afgelopen 100 dagen heeft bezocht om in aanmerking te komen voor promotie naar Vertrouwensniveau 3. Om promoties naar dit niveau uit te schakelen, kan je het aantal dagen hoger zetten dan de periode voor vertrouwensniveau 3." tl3_requires_topics_replied_to: "Minimum aantal topics waarop een lid moet hebben gereageerd in de laatste 100 dagen om in aanmerking te komen voor promotie naar trustlevel 3 (0 of hoger)" tl3_requires_topics_viewed: "Het percentage van topics gemaakt in de afgelopen 100 dagen dat een lid bekeken moet hebben om in aanmerking te komen voor promotie naar trustlevel 3. (0 to 100)" tl3_requires_posts_read: "Het percentage van berichten gemaakt in de afgelopen 100 dagen dat een lid bekeken moet hebben om in aanmerking te komen voor promotie naar trustlevel 3. (0 to 100)" @@ -909,8 +922,10 @@ nl: newuser_max_mentions_per_post: "Het maximum aantal @naam notificaties dat een nieuw lid kan gebruiken in een bericht. " newuser_max_replies_per_topic: "Het maximum aantal reacties dat een nieuw lid kan maken in een enkel topic totdat iemand terug reageert." max_mentions_per_post: "Het maximum aantal @naam notificaties iedereen kan gebruiken in een bericht." + max_users_notified_per_group_mention: "Maximum aantal gebruikers dat een notificatie kan krijgen als een groep genoemd wordt (boven dit aantal worden er geen notificaties verstuurd)." create_thumbnails: "Maak miniaturen en lightbox afbeeldingen die te groot zijn en niet in een post passen." email_time_window_mins: "Wacht (n) minuten met verzenden van notificaties, geef gebruikers de kans om hun berichten te bewerken en af te ronden." + private_email_time_window_seconds: "Wacht (n) minuten met het verzenden van e-mails met notificaties, om gebruikers de kans te geven om hun berichten te bewerken en af te ronden." email_posts_context: "Hoeveel voorgaande antwoorden bijvoegen als context in de notificatie e-mails." flush_timings_secs: "Hoe frequent we de timing data naar de server sturen, in seconden. " title_max_word_length: "De maximum toegestane woordlengte, in letters, in een topic titel." @@ -964,6 +979,8 @@ nl: disable_emails: "Voorkomt dat Discourse e-mails van welke soort dan ook verstuurd." strip_images_from_short_emails: "Verwijder afbeeldingen met grootte van minder dan 2800 Bytes uit e-mails." short_email_length: "Korte e-mail lengte in Bytes" + display_name_on_email_from: "Toon de volledige naam in het afzenderveld van e-mails." + unsubscribe_via_email_footer: "Voeg in de voettekst van verstuurde e-mails een link toe waarmee de ontvanger zich kan uitschrijven." pop3_polling_enabled: "Poll via POP3 voor antwoorden per e-mail." pop3_polling_ssl: "Gebruik SSL bij de verbinding naar de POP3 server. (Aanbevolen)" pop3_polling_period_mins: "De periode in minuten tussen controleren van het POP3 account voor e-mail. LET OP: vereist een herstart." @@ -990,6 +1007,7 @@ nl: automatically_download_gravatars: "Download Gravatars voor gebruikers bij account creatie of aanpassing van email." digest_topics: "Het maximum aantal topics dat in de e-maildigest opgenomen wordt." digest_min_excerpt_length: "Hoeveel karakters er per bericht getoond worden in de mail digest" + delete_digest_email_after_days: "Blokkeer e-mails met periodieke samenvattingen voor gebruikers die al meer dan (n) dagen niet meer op de site zijn gezien." disable_digest_emails: "Uitschakelen e-mails met korte verslagen voor alle gebruikers." detect_custom_avatars: "Wel of niet te verifiëren of gebruikers eigen profielfoto's hebben geüpload. " max_daily_gravatar_crawls: "Maximaal aantal keren op een dag dat Discourse Gravatar zal controleren voor aangepaste avatars" @@ -1001,6 +1019,7 @@ nl: anonymous_account_duration_minutes: "Ter bescherming van anonimiteit, maak een nieuw anonieme account aan elke N minuten voor elke gebruiker. Bijvoorbeeld: als ingesteld op 600 dan zal een nieuwe anonieme account worden aangemaakt als er 600 minuten zijn verstreken na het laatste bericht EN de gebruiker schakelt om naar anon." hide_user_profiles_from_public: "Uitschakelen gebruikerskaarten, gebruikersprofielen en het gebruikersoverzicht voor anonieme gebruikers." allow_profile_backgrounds: "Gebruikers mogen een profielachtergrond instellen." + sequential_replies_threshold: "Aantal berichten dat een gebruiker achter elkaar in één topic moet plaatsen voordat hij een melding krijgt over te veel opeenvolgende reacties." enable_mobile_theme: "Mobiele apparaten gebruiken een mobiel-vriendelijke theme met de mogelijkheid te schakelen naar de volledige site. Schakel deze optie uit als je een eigen stylesheet wil gebruiken die volledig responsive is." dominating_topic_minimum_percent: "Welk percentage van de berichten een gebruiker moet maken in een onderwerp voordat ze worden herinnerd aan het te veel domineren van een topic." daily_performance_report: "Analyseer elke dag NGINX logs en post een Alleen Voor Medewerkers topic met de details" @@ -1037,7 +1056,7 @@ nl: enable_emoji: "Inschakelen emoji" emoji_set: "Hoe wil je jouw emoji hebben?" enforce_square_emoji: "Forceer een vierkant aspect ratio bij alle emojis." - approve_post_count: "De hoeveelheid posts van een nieuwe gebruiker, dat moet worden goedgekeurd" + approve_post_count: "Het aantal berichten van een nieuwe of normale gebruiker dat moet worden goedgekeurd" approve_unless_trust_level: "Posts voor gebruikers onder dit vertrouwensniveau moeten worden goedgekeurd" notify_about_queued_posts_after: "Als er posts zijn die meer dan dit aantal uren klaar staan om te worden beoordeeld, dan zal er een e-mail worden gestuurd naar het contact e-mail. Zet op 0 om deze e-mails uit te schakelen." default_email_digest_frequency: "Hoe vaak ontvangen gebruikers standaard een samenvattings-e-mails." @@ -1052,6 +1071,7 @@ nl: default_other_dynamic_favicon: "Toon standaard een teller van nieuwe/bijgewerkte topics op browser icoon." default_other_disable_jump_reply: "Spring standaard niet naar de gebruiker post nadat ze hebben geantwoord." default_other_edit_history_public: "Maak standaard de aanpassingen aan de post openbaar." + default_other_like_notification_frequency: "Stuur gebruikers standaard een bericht bij een like" default_topics_automatic_unpin: "Standaard automatisch topics ontpinnen wanneer de gebruiker het einde van de pagina heeft bereikt." default_categories_watching: "Lijst van categorieën die standaard worden bekeken." default_categories_tracking: "Lijst van categorieën die standaard worden bijgehouden." @@ -1176,7 +1196,10 @@ nl: characters: "mag alleen nummers, letters en underscores bevatten" unique: "moet uniek zijn" blank: "mag niet leeg zijn" + must_begin_with_alphanumeric_or_underscore: "moet beginnen met een letter, een nummer of een laag streepje" + must_end_with_alphanumeric: "moet eindigen op een letter of een nummer" must_not_contain_two_special_chars_in_seq: "mag niet 2 of meer opvolgende speciale karakters bevatten (.-_)" + must_not_end_with_confusing_suffix: "mag niet eindigen op een verwarrend achtervoegsel zoals .json, .png etc." email: not_allowed: "is niet toegestaan vanaf die e-mailprovider. Gebruik een ander e-mailadres." blocked: "is niet toegestaan." @@ -1187,6 +1210,15 @@ nl: subject_template: one: "Eén vlag af te handelen" other: "%{count} vlaggen af te handelen" + unsubscribe_mailer: + text_body_template: | + Iemand (jij misschien?) heeft verzocht om op dit adres niet langer e-mails met nieuws van %{site_domain_name} te ontvangen. + Klik op de volgende link als je dit wil bevestigen: + + %{confirm_unsubscribe_link} + + + Als je deze e-mails wel wil blijven ontvangen, kan je deze e-mail negeren. invite_mailer: subject_template: "%{invitee_name} nodigt je uit voor '%{topic_title}' op %{site_domain_name}" text_body_template: | @@ -1232,8 +1264,34 @@ nl: subject_template: "[%{site_name}] E-mail Bezorgtest" new_version_mailer: subject_template: "[%{site_name}] Nieuwe Discourse-versie, update beschikbaar" + text_body_template: | + Hoera! Er is een nieuwe versie van [Discourse](http://www.discourse.org) beschikbaar! :) + + Jouw versie: %{installed_version} + Nieuwe versie: **%{new_version}** + + -Kijk wat er nieuw is in de [lijst met veranderingen op GitHub ](https://github.com/discourse/discourse/commits/master). + + - Werk Discourse bij door naar [%{base_url}/admin/upgrade](%{base_url}/admin/upgrade) te gaan. + + - Bezoek [meta.discourse.org](http://meta.discourse.org) voor nieuws, discussie en ondersteuning voor Discourse. new_version_mailer_with_notes: subject_template: "[%{site_name}] Update beschikbaar" + text_body_template: | + Hoera! Er is een nieuwe versie van [Discourse](http://www.discourse.org) beschikbaar! :) + + Jouw versie: %{installed_version} + Nieuwe versie: **%{new_version}** + + -Kijk wat er nieuw is in de [lijst met veranderingen op GitHub ](https://github.com/discourse/discourse/commits/master). + + - Werk Discourse bij door naar [%{base_url}/admin/upgrade](%{base_url}/admin/upgrade) te gaan. + + - Bezoek [meta.discourse.org](http://meta.discourse.org) voor nieuws, discussie en ondersteuning voor Discourse. + + ### Uitgaveopmerkingen + + %{notes} queued_posts_reminder: subject_template: one: "[%{site_name}] 1 bericht wacht op beoordeling" @@ -1257,6 +1315,20 @@ nl: system_messages: post_hidden: subject_template: "Bericht van %{site_name}: je bericht is verborgen wegens meldingen uit de community" + text_body_template: | + Hallo, + + Dit is een automatisch bericht van %{site_name} om je te laten weten dat je bericht is verborgen. + + %{base_url}%{url} + + %{flag_reason} + + Voordat dit bericht werd verborgen, hebben meerdere gebruikers het als ongewenst aangemeld. Denk er daarom goed over na hoe je je bericht aan de hand van hun commentaar aan kan passen. **Je kan je bericht na %{edit_delay} minuten wijzigen, waarna het automatisch weer zichtbaar wordt.** + + Maar als je bericht nogmaals door de gemeenschap wordt verborgen, blijft het verborgen tot een moderator of beheerder ernaar kijkt. Zij kunnen verder actie ondernemen, inclusief het mogelijk schorsen van je account. + + Voor meer hulp verwijzen we je door naar onze [richtlijnen](%{base_url}/guidelines). welcome_user: subject_template: "Welkom bij %{site_name}!" welcome_invite: @@ -1278,10 +1350,138 @@ nl: text_body_template: "Het terugzetten van de backup is geslaagd." restore_failed: subject_template: "Terugzetten van backup is mislukt" + bulk_invite_succeeded: + subject_template: "Massale uitnodiging aan gebruikers met succes verwerkt" + text_body_template: "Je bestand voor een massale uitnodiging aan gebruikers is verwerkt. Er zijn %{sent} uitnodigingen verstuurd." + bulk_invite_failed: + subject_template: "Massale uitnodiging aan gebruikers verwerkt, met fouten" csv_export_succeeded: subject_template: "Export succesvol afgerond." csv_export_failed: subject_template: "Export mislukt" + text_body_template: "Het spijt ons, maar de export is mislukt. Bekijk de logbestanden of neem contact op met de staf." + email_reject_insufficient_trust_level: + subject_template: "[%{site_name}] E-mailprobleem -- Vertrouwensniveau te laag" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Het vertrouwensniveau van je account is niet hoog genoeg om nieuwe topics aan dit e-mailadres te sturen. Als je denkt dat dit niet klopt, kan je contact opnemen met de staf. + email_reject_inactive_user: + subject_template: "[%{site_name}] E-mailprobleem -- Gebruiker inactief" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Het account dat bij dit e-mailadres hoort, is niet geactiveerd. Je moet je account activeren voordat je e-mails instuurt. + email_reject_blocked_user: + subject_template: "[%{site_name}] E-mailprobleem -- Geblokkeerde gebruiker" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Het account dat bij dit e-mailadres hoort, is geblokkeerd. + email_reject_reply_user_not_matching: + subject_template: "[%{site_name}] E-mailprobleem -- Reagerende gebruiker komt niet overeen" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Je reactie werd verstuurd van een ander e-mailadres dan we verwacht hadden, dus we weten niet zeker of dit dezelfde persoon is. Probeer de e-mail vanaf een ander adres te sturen, of neem contact op met de staf. + email_reject_no_message_id: + subject_template: "[%{site_name}] E-mailprobleem -- Geen message-id" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + We konden geen ‘message-id’ in de e-mail vinden. Probeer de e-mail vanaf een ander adres te sturen, of neem contact op met de staf. + email_reject_no_account: + subject_template: "[%{site_name}] E-mailprobleem -- Onbekende account" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + We konden geen accounts vinden die overeenkomen met je e-mailadres. Probeer de e-mail van een ander adres te sturen, of neem contact op met de staf. + email_reject_empty: + subject_template: "[%{site_name}] E-mailprobleem -- Geen inhoud" + text_body_template: |+ + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + We konden geen bruikbare tekst in je e-mail vinden. + + Als je deze melding krijgt en _wel_ een reactie had gestuurd, kan je het opnieuw proberen met een eenvoudigere opmaak. + + + email_reject_parsing: + subject_template: "[%{site_name}] E-mailprobleem -- Inhoud niet herkend" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + We konden je reactie niet in je e-mail vinden. **Zorg ervoor dat je reactie bovenaan de e-mail staat**, we kunnen ingebedde reacties niet verwerken. + email_reject_invalid_access: + subject_template: "[%{site_name}] E-mailprobleem -- Ongeldige toegang" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Je hebt niet het privilege om nieuwe topics in die categorie te plaatsen. Als je denkt dat dit niet klopt, kan je contact opnemen met de staf. + email_reject_strangers_not_allowed: + subject_template: "[%{site_name}] E-mailprobleem -- Ongeldige toegang" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + In de categorie waaraan je de e-mail stuurde, zijn alleen reacties van gebruikers met geldige accounts en bekende e-mailadressen toegestaan. Als je denkt dat dit niet klopt, kan je contact opnemen met de staf. + email_reject_invalid_post: + subject_template: "[%{site_name}] E-mailprobleem -- Foutmelding bij het plaatsen" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Dit kan bijvoorbeeld komen door complexe opmaak of door een te groot of te klein bericht. Probeer het opnieuw of plaats je bericht via de website als het nog steeds niet lukt. + email_reject_invalid_post_specified: + subject_template: "[%{site_name}] E-mailprobleem -- Foutmelding bij het plaatsen" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Reden: + + %{post_error} + + Probeer het opnieuw als je het probleem kan oplossen. + email_reject_rate_limit_specified: + subject_template: "[%{site_name}] E-mailprobleem -- Gelimiteerde snelheid" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Reden: %{rate_limit_description} + email_reject_invalid_post_action: + subject_template: "[%{site_name}] E-mailprobleem -- Ongeldige plaatsingsactie" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + De plaatsingsactie werd niet herkend. Probeer het opnieuw of plaats je bericht via de website als het nog steeds niet lukt. + email_reject_reply_key: + subject_template: "[%{site_name}] E-mailprobleem -- Onbekende antwoordsleutel" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + De antwoordsleutel in de e-mail is ongeldig of onbekend, dus we weten niet waar deze e-mail een reactie op is. Neem contact op met de staf. + email_reject_bad_destination_address: + subject_template: "[%{site_name}] E-mailprobleem -- Onbekend bestemmingsadres" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Geen van de bestemmingsadressen werd herkend. Zorg ervoor dat je je e-mail naar het juiste door de staf verstrekte e-mailadres stuurt. + email_reject_topic_not_found: + subject_template: "[%{site_name}] E-mailprobleem -- Topic niet gevonden" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Het topic waarop je reageert bestaat niet meer. Misschien is het verwijderd? Als je denkt dat dit niet klopt, kan je contact opnemen met de staf. + email_reject_topic_closed: + subject_template: "[%{site_name}] E-mailprobleem -- Topic gesloten" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Het topic waarop je reageert is op dit moment gesloten en accepteert geen reacties meer. Als je denkt dat dit niet klopt, kan je contact opnemen met de staf. + email_reject_auto_generated: + subject_template: "[%{site_name}] E-mailprobleem -- Automatisch gegenereerde reactie" + text_body_template: | + Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. + + Je e-mail is als ‘automatisch gegenereerd’ gemarkeerd, wat betekent dat het automatisch door een computer is geschreven in plaats van door een mens. Zulke e-mails kunnen wij niet accepteren. Als je denkt dat dit niet klopt, kan je contact opnemen met de staf. email_error_notification: subject_template: "[%{site_name}] Email probleem -- POP authenticatie fout" too_many_spam_flags: @@ -1328,6 +1528,7 @@ nl: subject_pm: "[PM]" user_notifications: previous_discussion: "Vorige reacties" + in_reply_to: "in reactie op" unsubscribe: title: "Uitschrijven" description: "Niet geïnteresseerd in deze e-mails? Geen probleem! Klik hieronder om direct uitgeschreven te worden:" @@ -1374,6 +1575,15 @@ nl: Klik op de volgende link om een wachtwoord in te stellen: %{base_url}/users/password-reset/%{email_token} + admin_login: + subject_template: "[%{site_name}] Login" + text_body_template: | + Iemand heeft geprobeerd om met je account op [%{site_name}](%{base_url}) in te loggen. + + Als jij het niet was, kan je deze e-mail gewoon negeren. + + Klik op de volgende link om in te loggen: + %{base_url}/users/admin-login/%{email_token} account_created: subject_template: "[%{site_name}] Uw Nieuwe Account" text_body_template: | @@ -1381,7 +1591,7 @@ nl: Klik op deze link om een wachtwoord in te stellen voor je nieuwe account: %{base_url}/users/password-reset/%{email_token} - confirm_new_email: + authorize_email: subject_template: "[%{site_name}] Bevestig je nieuwe e-mailadres" text_body_template: | Bevestig je nieuwe e-mailadres voor %{site_name} door op de volgende link te klikken: @@ -1416,6 +1626,7 @@ nl: Mocht je de bovenstaande link niet kunnen aanklikken, probeer deze dan te kopiëren en te plakken in de adresbalk van je web browser. page_not_found: + title: "Oeps! Die pagina bestaat niet of is privé." popular_topics: "Populair" recent_topics: "Recent" see_more: "Meer" @@ -1434,11 +1645,14 @@ nl: unauthorized: "Sorry, je mag dat bestand niet uploaden (toegestane extensies: %{authorized_extensions})." pasted_image_filename: "Geplakte afbeelding" store_failure: "Het opslaan van upload #%{upload_id} voor gebruiker #%{user_id} is mislukt." + file_missing: "Sorry, maar je moet een bestand leveren om te uploaden." attachments: too_large: "Sorry, het bestand dat je wil uploaden is te groot (maximum grootte is %{max_size_kb}%KB)." images: too_large: "De afbeelding die je wil uploaden is te groot (maximum grootte is %{max_size_kb}%KB). Verklein de afbeelding en probeer het opnieuw." size_not_found: "Het is niet gelukt de afmetingen van de afbeelding te bepalen. Misschien is het bestand corrupt?" + avatar: + missing: "Sorry, maar de avatar die je hebt geselecteerd, staat niet op de server. Kan je hem nog een keer proberen te uploaden?" flag_reason: sockpuppet: "Een nieuwe gebruiker maakte een nieuwe topic en een andere gebruiker reageerde - vanaf hetzelfde IP-adres. Zie de flag_sockpuppets instelling." spam_hosts: "Deze nieuwe gebruiker probeerde om meerdere berichten met links naar hetzelfde domein te plaatsen. Zie de newuser_spam_host_threshold instelling." @@ -1482,6 +1696,8 @@ nl: Deze badge wordt toegekend voor het aanmaken van topic dat 10 likes krijgt. Mooi gedaan! good_post: | Deze badge wordt toegekend voor het geven van een antwoord dat 25 likes krijgt. Goed bezig! + good_topic: | + Deze badge wordt toegekend voor het plaatsen van een topic dat 25 likes krijgt. Goed bezig! great_post: | Deze badge wordt toegekend voor het aanmaken van post die 50 likes krijgt. Wow! great_topic: | @@ -1504,6 +1720,8 @@ nl: performance_report: initial_post_raw: Deze topic bevat dagelijkse performance rapporten van je site initial_topic_title: Website performance rapporten + topic_invite: + user_exists: "Excuus, die gebruiker is al uitgenodigd. Je kan een gebruiker maar een keer voor een topic uitnodigen." time: <<: *datetime_formats activemodel: diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 11ba25a367..0437c8025d 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -30,6 +30,9 @@ sk: emails: incoming: default_subject: "Prijatý email od %{email}" + errors: + inactive_user_error: "Nastane, keď odosielateľ nie je aktívny." + blocked_user_error: "Nastane, keď odosielateľ bol zablokovaný." errors: &errors format: '%{attribute} %{message}' messages: @@ -820,7 +823,6 @@ sk: suppress_reply_directly_above: "Nezobrazovať rozbaľovacie tlačidlo v-odpovedi-na ak je len jedna odpoveď priamo nad príspevkom." suppress_reply_when_quoting: "Nezobrazovať rozbaľovacie tlačidlo v-odpovedi-na ak príspevok cituje odpoveď." max_reply_history: "Maximálny počet odpovedí ktoré sa zobrazia po rozbalení v-odpovedi-na" - experimental_reply_expansion: "Skryť prostredné odpovede v prípade rozbaľovania odpovede (experimentálne)" topics_per_period_in_top_summary: "Zobrazovaný počet najlepších tém vo východzom zozname najlepších tém." topics_per_period_in_top_page: "Zobrazovaný počet najlepších tém vzozname po rozbalení 'Ukáž viac' najlepších tém." redirect_users_to_top_page: "Automaticky presmeruj nových a dlho neprihlásených užívateľov na hlavnú stránku. " @@ -847,6 +849,7 @@ sk: max_username_length: "Maximálna dĺžka používateľského mena v znakoch." reserved_usernames: "Používateľské mená, ktorých registrácia nie je povolená." min_password_length: "Minimálna dĺžka hesla." + min_admin_password_length: "Minimálna dĺžka hesla pre Administrátora." block_common_passwords: "Nepovoliť heslá, ktoré sú v zozname 10 000 najbežnejších hesiel." enable_sso: "Povolit prihlasovanie pomocou externej stránky (VAROVANIE: POUŽÍVATEĽSKÉ EAMILOVÉ ADRESY *MUSIA* BYŤ OVERENÉ EXTERNOU STRÁNKOU)" enable_sso_provider: "Implementácia protokolu Discourse SSO poskytovateľa na koncovom bode /session/sso_provider, vyžaduje nastavenie sso_secret" @@ -1090,7 +1093,6 @@ sk: enable_emoji: "Zapnúť emoji" emoji_set: "Aké emoji chcete mať?" enforce_square_emoji: "Vynútiť švorcový pomer strán na všetkých emoji." - approve_post_count: "Koľko úvodných prispevkov od nového používateľa musí byť schválených" approve_unless_trust_level: "Príspevky používateľov pod touto úrovňou důvery musia byť schválené" notify_about_queued_posts_after: "Ak existujú príspevky, ktoré čakali na schválenie viac ako uvedený počet hodín, na kontaktný email bude zaslaná správa. Na vypnutie emailov nastavte 0." default_email_digest_frequency: "Ako často štandardne používatelia dostávajú sumárny email." @@ -1138,7 +1140,6 @@ sk: moved_post: "%{display_username} presunul Váš príspevok do %{link}" private_message: "%{display_username} vám poslal správu: %{link}" invited_to_private_message: "%{display_username} Vás pozval k správe: %{link}" - invited_to_topic: "%{display_username} Vás pozval k téme: %{link}" invitee_accepted: "%{display_username} prijal vaše pozvanie" linked: "%{display_username} odkazuje na Vás v %{link}" granted_badge: "Získali ste %{link}" @@ -1243,8 +1244,6 @@ sk: characters: "musí obsahovať len čísla, písmená a podržníky" unique: "musí byť jedinečné" blank: "musí existovať" - must_begin_with_alphanumeric: "musí začínať písmenom, číslom alebo podržítkom" - must_end_with_alphanumeric: "musí končiť písmenom, číslom alebo podržítkom" must_not_contain_two_special_chars_in_seq: "nesmie obsahovať reťazec 2 alebo viac špeciálnych znakov po sebe (.-_)" must_not_end_with_confusing_suffix: "nesmie byť ukončené mätúcou príponou ako napríklad .json alebo .png a podobne." email: @@ -1253,6 +1252,11 @@ sk: ip_address: blocked: "Nové registrácie nie sú povolené z Vašej IP adresy." max_new_accounts_per_registration_ip: "Nové registrácie nie sú povolené z Vašej IP adresy (dosiahnutý limit). Kontaktujte člena obsluhy. " + flags_reminder: + subject_template: + one: "1 značká čaká na zpracovanie" + few: "%{count} značky čakajú na zpracovanie" + other: "%{count} značiek čaká na zpracovanie" unsubscribe_mailer: subject_template: "Potvrďte, že už ďalej nechcete dostávať aktualizácie z %{site_title}" text_body_template: | @@ -1306,89 +1310,10 @@ sk: (Ak vyššie uvedený odkaz vyexpiroval, vyberte "Zabudol som heslo" keď sa prihlasujete pomocou svojej emailovej adresy.) test_mailer: subject_template: "[%{site_name}] Overenie doručiteľnosti emailu" - text_body_template: | - Toto je testovací email z - - [**%{base_url}**][0] - - Doručovanie emailov je komplikované. Nižšie je niekoľko dôležitých vecí, ktoré by ste mali skontrolovať ako prvé: - - - *Uistite sa*, že ste pre oznamovací email v nastaveniach stránky nastavili správnu emailovú adresu Od. **Doména vašich emailov bude kontrolované voči doméne, ktorú ste nastavili v poli "od"**. - - - Zistite, ako vo Vašom emailovom programe zobraziť zdojový text emailov. Tým môžete preskúmať emailové hlavičky. V Gmail je to voľba "show original" v položkovom zozname menu na pravej hornej strane každého emailu. - - - **DÔLEŽITÉ:** Zadal Váš poskytovateľ pripojenia reverzné DNS záznamy, abe spojil doménové mená a IP adresy z ktorých odosielate emaily?? [Overte si Váš obrátený PTR záznam][2]. Je veľmi nepravdepodobné, že budé akýkoľvek z Vašich emailov doručený ak nie je správne zadaný obrátený DNS záznam. - - - Je Váš doménový [SPF záznam][8] správny? [Overte si Váš SPF záznam][1]. Uvedomte si, že TXT je správny oficiálny typ pre SPF. - - - Je Váš doménový [DKIM záznam][3] správny? Tot výrazne vylepší Vašu duručiteľnosť emailov. [Overte si Váš DKIM záznam][7]. - - - Ak prevádzkujete vlastný email server, uistite sa, že jeho IP adresy [nie sú na žiadnej emailovej čiernej listine][4]. Tiež sa uistite, pomocou HALO správy, že určite posiela správne adresy, ktoré sa prekladajú v DNS. Ak nie, tak to spôsobí, že Vaš email bude zamietnutý množstvom emailových služieb. - - (*Jednoduchou* možnosťou je vytvorenie účtu zdarma na [Mandrill][md] alebo [Mailgun][mg] alebo [Mailjet][mj], ktoré majú štedré plány zdarma a pre malé komunity sú dostatočné. I tak budete musieť nastaviť záznamy SPF a DKIM vo Vašom DNS!) - - Dúfame, že ste tento test doručiteľnosti obdržali v poriadku.! - - Veľa šťastia, - - Vaši priatelia z [Discourse](http://www.discourse.org) - - [0]: %{base_url} - [1]: http://www.kitterman.com/spf/validate.html - [2]: http://mxtoolbox.com/ReverseLookup.aspx - [3]: http://www.dkim.org/ - [4]: http://whatismyipaddress.com/blacklist-check - [7]: http://dkimcore.org/tools/dkimrecordcheck.html - [8]: http://www.openspf.org/SPF_Record_Syntax - [md]: http://mandrill.com - [mg]: http://www.mailgun.com/ - [mj]: https://www.mailjet.com/pricing new_version_mailer: subject_template: "[%{site_name}] Nové verzia Discourse, dostupná aktualizácia" - text_body_template: | - Je dostupná nová verzia [Discourse](http://www.discourse.org). - - Vaša verzia: %{installed_version} - Nová verzia: **%{new_version}** - - Môžete: - - - Si pozrieť, čo je nového pomocou [zoznamu zmien na GitHub](https://github.com/discourse/discourse/commits/master). - - - Aktualizovať z Vášho prehliadača [%{base_url}/admin/upgrade](%{base_url}/admin/upgrade). - - - Navštíviť [meta.discourse.org](http://meta.discourse.org) a zistiť novinky, prehliadnuť si diskusiu alebo požiadať o podporu pre Discourse. new_version_mailer_with_notes: subject_template: "[%{site_name}] aktualizácia dostupná" - text_body_template: | - Je dostupná nová verzia [Discourse](http://www.discourse.org). - - Vaša verzia: %{installed_version} - Nová verzia: **%{new_version}** - - Môžete: - - - Si pozrieť, čo je nového pomocou [zoznamu zmien na GitHub](https://github.com/discourse/discourse/commits/master). - - - Aktualizovať z Vášho prehliadača [%{base_url}/admin/upgrade](%{base_url}/admin/upgrade). - - - Navštíviť [meta.discourse.org](http://meta.discourse.org) a zistiť novinky, prehliadnuť si diskusiu alebo požiadať o podporu pre Discourse. - - ### Poznámky k vydaniu - - %{notes} - flags_reminder: - flags_were_submitted: - one: "Tieto príznaky boli odoslané pred viac ako 1hodinou." - few: "Tieto príznaky boli odoslané pred viac ako %{count} hodinami.." - other: "Tieto príznaky boli odoslané pred viac ako %{count} hodinami." - please_review: "Prosím o ich preverenie." - post_number: "príspevok" - how_to_disable: 'Interval zasielania upozornení emailom možete vypnúť alebo zmeniť v Nastaveniach pomocou možnosti "upozorniť o značke po".' - subject_template: - one: "1 značká čaká na zpracovanie" - few: "%{count} značky čakajú na zpracovanie" - other: "%{count} značiek čaká na zpracovanie" queued_posts_reminder: subject_template: one: "%{site_name}] 1 príspevok čaká na revíziu" @@ -1651,8 +1576,8 @@ sk: title: "Odhlásiť" description: "Nezaujímajú Vás už tieto emaily? Žiadny problém! Pre okamžité odhlásenie kliknite nižšie: " reply_by_email: "[Pozrite tému](%{base_url}%{url}), alebo zareagujte odpoveďou na tento email" - visit_link_to_respond: "[Pozrite tému](%{base_url}%{url}) pre reakciu" reply_by_email_pm: "[Pozrite správu](%{base_url}%{url}), alebo zareagujte odpoveďou na tento email" + visit_link_to_respond: "[Pozrite tému](%{base_url}%{url}) pre reakciu" visit_link_to_respond_pm: "[Pozrite správu](%{base_url}%{url}) pre reakciu" posted_by: "Príspevok od %{username} dňa %{post_date}" user_invited_to_private_message_pm: @@ -1660,7 +1585,6 @@ sk: user_invited_to_private_message_pm_staged: subject_template: "[%{site_name}] %{username} Vás pozval k správe '%{topic_title}'" user_invited_to_topic: - subject_template: "[%{site_name}] %{username} Vás pozval k téme '%{topic_title}'" text_body_template: |2 %{username} Vás pozval do diskusie @@ -1790,7 +1714,7 @@ sk: Kliknite na nasledujúci odkaz pre nastavenie hesla k Vášmu novému účtu: %{base_url}/users/password-reset/%{email_token} - confirm_new_email: + authorize_email: subject_template: "[%{site_name}] Potvrďte Vašu novú email adresu" text_body_template: | Potvrďte Vašu novú emailovú adresu pre %{site_name} kliknutím na nasledujúci odkaz: @@ -1801,7 +1725,7 @@ sk: signup: subject_template: "[%{site_name}] Potvrďte Váš nový účet" page_not_found: - title: "Stránka ktorú požadujete neexistuje alebo je súkromná." + title: "Stránka neexistuje alebo je privátna." popular_topics: "Populárne" recent_topics: "Nedávne" see_more: "Viac" diff --git a/public/403.zh_CN.html b/public/403.zh_CN.html index 15657d1f7f..3adf6ed2bc 100644 --- a/public/403.zh_CN.html +++ b/public/403.zh_CN.html @@ -1,6 +1,6 @@ -操作行为禁止 (403) +禁止操作行为 (403) diff --git a/app/views/list/list.erb b/app/views/list/list.erb index 386a74f348..23f6f8cc32 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -25,9 +25,9 @@ <%= page_links(t) %> <% if (!@category || @category.has_children?) && t.category %> - [<%= t.category.name %>] + [<%= t.category.name %>] <% end %> - '>(<%= t.posts_count %>) + '>(<%= t.posts_count %>)
    <% end %>
    diff --git a/app/views/topics/show.html.erb b/app/views/topics/show.html.erb index 583a0d6402..010999cc92 100644 --- a/app/views/topics/show.html.erb +++ b/app/views/topics/show.html.erb @@ -20,7 +20,7 @@ <%= server_plugin_outlet "topic_header" %>
    -<%- unless mobile_view? %> +<%- if include_crawler_content? %> <% @topic_view.posts.each do |post| %>
    From 0ea20f2d775eaa683d9d267110dc4e3ed800e1ff Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Mar 2016 15:46:16 +1100 Subject: [PATCH 117/123] mock was causing spec to fail --- spec/controllers/application_controller_spec.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 84e770e439..1d46068aac 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -66,9 +66,6 @@ describe TopicsController do render_views context "when not a crawler" do - before do - CrawlerDetection.expects(:crawler?).returns(false) - end it "renders with the application layout" do get :show, topic_id: topic.id, slug: topic.slug expect(response).to render_template(layout: 'application') @@ -77,10 +74,8 @@ describe TopicsController do end context "when a crawler" do - before do - CrawlerDetection.expects(:crawler?).returns(true) - end it "renders with the crawler layout" do + request.env["HTTP_USER_AGENT"] = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" get :show, topic_id: topic.id, slug: topic.slug expect(response).to render_template(layout: 'crawler') assert_select "meta[name=fragment]", false, "it doesn't have the meta tag" From 49fe5a93b142771930ad16972db312ae969a08cf Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 17 Mar 2016 16:56:47 +1100 Subject: [PATCH 118/123] tweak logo image --- app/views/layouts/crawler.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/layouts/crawler.html.erb b/app/views/layouts/crawler.html.erb index 82091e3983..a2dfcf11a0 100644 --- a/app/views/layouts/crawler.html.erb +++ b/app/views/layouts/crawler.html.erb @@ -12,7 +12,8 @@ <%= yield :head %>