From dedd5e0e6051d89b7dfbf4b8cc218586b3f48de4 Mon Sep 17 00:00:00 2001 From: Tarek Khalil <45508821+khalilovcmded@users.noreply.github.com> Date: Tue, 5 Mar 2019 18:01:25 +0000 Subject: [PATCH 01/67] FIX: WebhookUserSerializer spec failure --- spec/serializers/web_hook_user_serializer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/serializers/web_hook_user_serializer_spec.rb b/spec/serializers/web_hook_user_serializer_spec.rb index 65b5c8d876d..f74d5cfce71 100644 --- a/spec/serializers/web_hook_user_serializer_spec.rb +++ b/spec/serializers/web_hook_user_serializer_spec.rb @@ -21,7 +21,7 @@ RSpec.describe WebHookUserSerializer do it 'should only include the required keys' do count = serializer.as_json.keys.count - difference = count - 45 + difference = count - 46 expect(difference).to eq(0), lambda { message = "" From b0d93a38e85896e9f789525ce8db840f59ef3b6e Mon Sep 17 00:00:00 2001 From: Jeff Wong Date: Tue, 5 Mar 2019 10:39:42 -0800 Subject: [PATCH 02/67] FEATURE: Add plugin html hook to insert html before any other scripts --- app/views/layouts/application.html.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f6a9b5613d7..a01460ce174 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -17,6 +17,8 @@ <%- end %> + <%= build_plugin_html 'server:before-script-load' %> + <%= preload_script "locales/#{I18n.locale}" %> <%= preload_script "ember_jquery" %> <%= preload_script "preload-store" %> From 84892429ed69f8491e9dc004d2598f1447b0351a Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 5 Mar 2019 14:50:27 -0500 Subject: [PATCH 03/67] UX: Improve layout of invite table --- .../javascripts/discourse/templates/user-invited-show.hbs | 5 +++-- app/assets/stylesheets/desktop/user.scss | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index dcd09ac5892..f04959ea92a 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -81,7 +81,7 @@ {{else}} {{unbound invite.email}} {{format-date invite.created_at}} - + {{#if invite.expired}} {{i18n 'user.invited.expired'}}      @@ -91,7 +91,8 @@ {{else}} {{d-button icon="times" action=(action "rescind") actionParam=invite label="user.invited.rescind"}} {{/if}} -      + + {{#if invite.reinvited}} {{i18n 'user.invited.reinvited'}} {{else}} diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index f04ae567021..4e5cdb6e303 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -115,6 +115,14 @@ .user-invite-list { width: 100%; margin-top: 15px; + tr { + td { + padding: .667em; + } + td:nth-of-type(n+3) { + text-align: right; + } + } } .user-invite-search { From 2ea77073aedab229f031b82a0f04bab68aca82ce Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 5 Mar 2019 15:12:10 -0500 Subject: [PATCH 04/67] prettier --- app/assets/stylesheets/desktop/user.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 4e5cdb6e303..7c5cfaee3c0 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -117,9 +117,9 @@ margin-top: 15px; tr { td { - padding: .667em; + padding: 0.667em; } - td:nth-of-type(n+3) { + td:nth-of-type(n + 3) { text-align: right; } } From 345f6237cb0b945e5fe37f7c4445fed35adf4976 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 5 Mar 2019 22:18:49 +0100 Subject: [PATCH 05/67] Bump onebox version https://github.com/discourse/onebox/commit/f2b361fc286844dd83df74443259e3b760ff29c9 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 54797b5cf8c..6a8c56eaa7c 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.79' +gem 'onebox', '1.8.80' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 48f6a2610ca..b9032276dae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,7 +261,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.79) + onebox (1.8.80) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -515,7 +515,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.79) + onebox (= 1.8.80) openid-redis-store pg pry-nav From 33129efdb5e291feb1c5ff45f5cbfb179e00d23c Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 5 Mar 2019 22:51:02 +0100 Subject: [PATCH 06/67] Revert "Bump onebox version" This reverts commit 345f6237cb0b945e5fe37f7c4445fed35adf4976. --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 6a8c56eaa7c..54797b5cf8c 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.80' +gem 'onebox', '1.8.79' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index b9032276dae..48f6a2610ca 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,7 +261,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.80) + onebox (1.8.79) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -515,7 +515,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.80) + onebox (= 1.8.79) openid-redis-store pg pry-nav From b5804c410e205ee169197d11158d5c2e09999bbd Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 5 Mar 2019 20:16:12 -0500 Subject: [PATCH 07/67] UX: Add slash to mobile category page totals --- .../discourse/templates/mobile/components/categories-only.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs b/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs index 42b25ef1802..fda2b84c037 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/categories-only.hbs @@ -41,11 +41,11 @@
{{number c.topics_all_time}}
{{i18n 'all_time'}}
{{#if c.pickMonth}} -
{{number c.topics_month}}
{{i18n 'month'}}
+
{{number c.topics_month}}
/ {{i18n 'month'}}
{{/if}} {{#if c.pickWeek}} -
{{number c.topics_week}}
{{i18n 'week'}}
+
{{number c.topics_week}}
/ {{i18n 'week'}}
{{/if}} From fcfbdbc9c1c1cad50da92ad5fd3d314c9a0272fc Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 5 Mar 2019 17:38:12 -0800 Subject: [PATCH 08/67] add BLEXbot to blacklisted crawlers --- config/site_settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index 2acedd3ec46..627f4d5b07d 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1251,7 +1251,7 @@ security: list_type: compact blacklisted_crawler_user_agents: type: list - default: "mauibot|semrushbot|ahrefsbot" + default: "mauibot|semrushbot|ahrefsbot|blexbot" list_type: compact slow_down_crawler_user_agents: type: list From 86f9ace0eac320f55da6f9c13b3fd60f0a5cb96f Mon Sep 17 00:00:00 2001 From: Kris Date: Tue, 5 Mar 2019 22:10:12 -0500 Subject: [PATCH 09/67] UX: Previous positioning was better for the image resizer --- app/assets/stylesheets/common/d-editor.scss | 28 ++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 073c8ded119..b29aeb16f2e 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -203,8 +203,23 @@ border: 1px solid $primary-low; } +.d-editor-preview img { + padding-bottom: 1.4em; + &.emoji, + &.avatar { + padding-bottom: 0; + } +} + .d-editor-preview .image-wrapper { position: relative; + display: inline-block; + padding-bottom: 1.4em; + + img { + padding-bottom: 0; + } + &:hover { .button-wrapper { opacity: 0.9; @@ -212,21 +227,22 @@ } .button-wrapper { opacity: 0; - background: $secondary; position: absolute; transition: all 0.25s; display: flex; align-items: center; - bottom: 0.75em; - left: 0.75em; - box-shadow: shadow("dropdown"); + bottom: 0; + left: 0; .separator { - color: $primary-low; + color: $primary-low-mid; } .scale-btn { color: $tertiary; - padding: 0.2em 0.6em; + padding: 0 0.4em; + &:first-of-type { + padding-left: 0; + } &.active { font-weight: bold; From 1b1f9831b04d9a3af82eb38c2c0e46f6a6a7a217 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 6 Mar 2019 11:58:41 +0530 Subject: [PATCH 10/67] Bump onebox version discourse/onebox@4dd5a62 --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 54797b5cf8c..836fbf583b5 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.79' +gem 'onebox', '1.8.81' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 48f6a2610ca..2fda9211997 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,7 +261,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.79) + onebox (1.8.81) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -515,7 +515,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.79) + onebox (= 1.8.81) openid-redis-store pg pry-nav From 8d7c10f7f2a13020ccd6af649509591d4aff9643 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 6 Mar 2019 17:44:08 +1100 Subject: [PATCH 11/67] FIX: allow moderators to create categories setting broken Mods require visibility to everyone group cause category dialogs need to know about this. If the site setting `allow moderators to create categories` will not function without this Note there is no security expansion of rights here, the group is technically empty anyway and it always looks exactly the same on all discourse instances --- app/models/group.rb | 2 +- spec/models/group_spec.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/group.rb b/app/models/group.rb index 0f2959048a3..65bc2c7fd49 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -290,7 +290,7 @@ class Group < ActiveRecord::Base # way to have the membership in a table case name when :everyone - group.visibility_level = Group.visibility_levels[:owners] + group.visibility_level = Group.visibility_levels[:staff] group.save! return group when :moderators diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 8419e89976c..c70570411b5 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -221,9 +221,9 @@ describe Group do end describe '.refresh_automatic_group!' do - it "makes sure the everyone group is not visible" do + it "makes sure the everyone group is not visible except to staff" do g = Group.refresh_automatic_group!(:everyone) - expect(g.visibility_level).to eq(Group.visibility_levels[:owners]) + expect(g.visibility_level).to eq(Group.visibility_levels[:staff]) end it "ensures that the moderators group is messageable by all" do From b2187301fdf70efc42df7356b176c9c53129359c Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 6 Mar 2019 18:38:49 +1100 Subject: [PATCH 12/67] FEATURE: allow users to easily track/watch/mute topics via email If you reply to an email with the word "mute" a topic will be muted If you reply to an email with the word "track" a topic will be tracked If you reply to an email with the word "watch" a topic will be watched These ninja command can help advanced mailing list ex-users, saves a trip to the website --- lib/email/receiver.rb | 18 ++++++++++++ spec/components/email/receiver_spec.rb | 38 +++++++++++++++++++++----- spec/fixtures/emails/mute.eml | 10 +++++++ spec/fixtures/emails/track.eml | 10 +++++++ spec/fixtures/emails/watch.eml | 10 +++++++ 5 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 spec/fixtures/emails/mute.eml create mode 100644 spec/fixtures/emails/track.eml create mode 100644 spec/fixtures/emails/watch.eml diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 736626194d7..c7f706d05c2 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -899,6 +899,22 @@ module Email create_post_with_attachments(options) end + def notification_level_for(body) + # since we are stripping save all this work on long replies + return nil if body.length > 40 + + body = body.strip.downcase + case body + when "mute" + NotificationLevels.topic_levels[:muted] + when "track" + NotificationLevels.topic_levels[:tracking] + when "watch" + NotificationLevels.topic_levels[:watching] + else nil + end + end + def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? raise BouncedEmailError if options[:bounce] && options[:topic].archetype != Archetype.private_message @@ -908,6 +924,8 @@ module Email if post_action_type = post_action_for(options[:raw]) create_post_action(options[:user], options[:post], post_action_type) + elsif notification_level = notification_level_for(options[:raw]) + TopicUser.change(options[:user].id, options[:post].topic_id, notification_level: notification_level) else raise TopicClosedError if options[:topic].closed? options[:topic_id] = options[:topic].id diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index f243065e490..a87629e00f6 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -251,6 +251,10 @@ describe Email::Receiver do ) end + let :topic_user do + TopicUser.find_by(topic_id: topic.id, user_id: user.id) + end + 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 } @@ -285,14 +289,34 @@ describe Email::Receiver do expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicNotFoundError) end - it "raises a TopicClosedError when the topic was closed" do - topic.update_columns(closed: true) - expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError) - end + context "a closed topic" do - it "does not raise TopicClosedError when performing a like action" do - topic.update_columns(closed: true) - expect { process(:like) }.to change(PostAction, :count) + before do + topic.update_columns(closed: true) + end + + it "raises a TopicClosedError when the topic was closed" do + expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError) + end + + it "Can watch topics via the watch command" do + # TODO support other locales as well, the tricky thing is that these string live in + # client.yml not on server yml so it is a bit tricky to find + + topic.update_columns(closed: true) + process(:watch) + expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:watching]) + end + + it "Can mute topics via the mute command" do + process(:mute) + expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:muted]) + end + + it "can track a topic via the track command" do + process(:track) + expect(topic_user.notification_level).to eq(NotificationLevels.topic_levels[:tracking]) + end end it "raises an InvalidPost when there was an error while creating the post" do diff --git a/spec/fixtures/emails/mute.eml b/spec/fixtures/emails/mute.eml new file mode 100644 index 00000000000..62dea407517 --- /dev/null +++ b/spec/fixtures/emails/mute.eml @@ -0,0 +1,10 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <13@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +mute diff --git a/spec/fixtures/emails/track.eml b/spec/fixtures/emails/track.eml new file mode 100644 index 00000000000..a7d7ad10dfb --- /dev/null +++ b/spec/fixtures/emails/track.eml @@ -0,0 +1,10 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <13@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +track diff --git a/spec/fixtures/emails/watch.eml b/spec/fixtures/emails/watch.eml new file mode 100644 index 00000000000..446ff77b0d7 --- /dev/null +++ b/spec/fixtures/emails/watch.eml @@ -0,0 +1,10 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <13@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +watch From 05ebb52ec4024ef9d24594b946520e7200dd8447 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 6 Mar 2019 14:32:25 +0530 Subject: [PATCH 13/67] FEATURE: defer flags when deleting child replies (#7111) --- .../javascripts/admin/models/flagged-post.js.es6 | 4 +++- app/assets/javascripts/discourse/models/post.js.es6 | 4 ++-- app/controllers/posts_controller.rb | 6 ++++-- lib/post_destroyer.rb | 2 +- spec/requests/posts_controller_spec.rb | 13 ++++++++++--- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/admin/models/flagged-post.js.es6 b/app/assets/javascripts/admin/models/flagged-post.js.es6 index fde2cdf01a4..13d69bf6650 100644 --- a/app/assets/javascripts/admin/models/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/models/flagged-post.js.es6 @@ -136,7 +136,9 @@ export default Post.extend({ label: I18n.t("yes_value"), class: "btn-danger", callback() { - Post.deleteMany(replies.map(r => r.id), { deferFlags: true }) + Post.deleteMany(replies.map(r => r.id), { + agreeWithFirstReplyFlag: false + }) .then(action) .then(resolve) .catch(error => { diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 8da229cfb27..9202d581e05 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -378,10 +378,10 @@ Post.reopenClass({ }); }, - deleteMany(post_ids, { deferFlags = false } = {}) { + deleteMany(post_ids, { agreeWithFirstReplyFlag = true } = {}) { return ajax("/posts/destroy_many", { type: "DELETE", - data: { post_ids, defer_flags: deferFlags } + data: { post_ids, agree_with_first_reply_flag: agreeWithFirstReplyFlag } }); }, diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 6820ed87de9..8317bb665c8 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -336,7 +336,7 @@ class PostsController < ApplicationController def destroy_many params.require(:post_ids) - defer_flags = params[:defer_flags] || false + agree_with_first_reply_flag = (params[:agree_with_first_reply_flag] || true).to_s == "true" posts = Post.where(id: post_ids_including_replies) raise Discourse::InvalidParameters.new(:post_ids) if posts.blank? @@ -345,7 +345,9 @@ class PostsController < ApplicationController posts.each { |p| guardian.ensure_can_delete!(p) } Post.transaction do - posts.each { |p| PostDestroyer.new(current_user, p, defer_flags: defer_flags).destroy } + posts.each_with_index do |p, i| + PostDestroyer.new(current_user, p, defer_flags: !(agree_with_first_reply_flag && i == 0)).destroy + end end render body: nil diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index cf36b72d6f9..54e4451fbfe 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -147,7 +147,7 @@ class PostDestroyer update_user_counts TopicUser.update_post_action_cache(post_id: @post.id) DB.after_commit do - if @opts[:defer_flags].to_s == "true" + if @opts[:defer_flags] defer_flags else agree_with_flags diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb index 6c400cd1392..0829f3ee0ec 100644 --- a/spec/requests/posts_controller_spec.rb +++ b/spec/requests/posts_controller_spec.rb @@ -248,15 +248,22 @@ describe PostsController do let(:moderator) { Fabricate(:moderator) } before do + sign_in(moderator) PostAction.act(moderator, post1, PostActionType.types[:off_topic]) PostAction.act(moderator, post2, PostActionType.types[:off_topic]) Jobs::SendSystemMessage.clear end - it "defers the posts" do - sign_in(moderator) + it "defers the child posts by default" do expect(PostAction.flagged_posts_count).to eq(2) - delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id], defer_flags: true } + delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id] } + expect(Jobs::SendSystemMessage.jobs.size).to eq(1) + expect(PostAction.flagged_posts_count).to eq(0) + end + + it "can defer all posts based on `agree_with_first_reply_flag` param" do + expect(PostAction.flagged_posts_count).to eq(2) + delete "/posts/destroy_many.json", params: { post_ids: [post1.id, post2.id], agree_with_first_reply_flag: false } expect(Jobs::SendSystemMessage.jobs.size).to eq(0) expect(PostAction.flagged_posts_count).to eq(0) end From b58eea1fcba586544dcf0f00731d3a225c2dd4ab Mon Sep 17 00:00:00 2001 From: Tarek Khalil <45508821+khalilovcmded@users.noreply.github.com> Date: Wed, 6 Mar 2019 09:20:45 +0000 Subject: [PATCH 14/67] FEATURE: Hide ignored user's Original Post content (#7113) * FEATURE: Hide ignored user's Original Post content --- .../javascripts/discourse/lib/transform-post.js.es6 | 1 + app/assets/javascripts/discourse/widgets/post.js.es6 | 3 +++ app/assets/stylesheets/common/base/topic-post.scss | 4 ++++ app/serializers/basic_post_serializer.rb | 10 +++++++++- config/locales/server.en.yml | 3 +++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index c0371aa157e..b63c2249147 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -80,6 +80,7 @@ export function transformBasicPost(post) { expandablePost: false, replyCount: post.reply_count, locked: post.locked, + ignored: post.ignored, userCustomFields: post.user_custom_fields }; diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 5aa17d79c99..fbbda456aa5 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -608,6 +608,9 @@ export default createWidget("post", { } else { classNames.push("regular"); } + if (attrs.ignored) { + classNames.push("post-ignored"); + } if (addPostClassesCallbacks) { for (let i = 0; i < addPostClassesCallbacks.length; i++) { let pluginClasses = addPostClassesCallbacks[i].call(this, attrs); diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index e17493e8830..91463df6301 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -214,6 +214,10 @@ aside.quote { margin: -2px; } +.post-ignored { + font-style: italic; +} + .post-action { .undo-action, .act-action { diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb index bcc75f776e1..014421decfb 100644 --- a/app/serializers/basic_post_serializer.rb +++ b/app/serializers/basic_post_serializer.rb @@ -6,7 +6,8 @@ class BasicPostSerializer < ApplicationSerializer :avatar_template, :created_at, :cooked, - :cooked_hidden + :cooked_hidden, + :ignored def name object.user && object.user.name @@ -35,11 +36,18 @@ class BasicPostSerializer < ApplicationSerializer else I18n.t('flagging.user_must_edit') end + elsif ignored + I18n.t('ignored.hidden_content') else object.filter_quotes(@parent_post) end end + def ignored + object.is_first_post? && IgnoredUser.where(user_id: scope.current_user&.id, + ignored_user_id: object.user_id).present? + end + def include_name? SiteSetting.enable_names? end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d1013cc4589..e578529144f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -870,6 +870,9 @@ en: you_must_edit: '

Your post was flagged by the community. Please see your messages.

' user_must_edit: "

This post was flagged by the community and is temporarily hidden.

" + ignored: + hidden_content: '

Hidden content

' + archetypes: regular: title: "Regular Topic" From f7c4d8c8f9b5a50175cd2bba4ab6b070d6e740ab Mon Sep 17 00:00:00 2001 From: Tarek Khalil <45508821+khalilovcmded@users.noreply.github.com> Date: Wed, 6 Mar 2019 09:48:20 +0000 Subject: [PATCH 15/67] FIX: WebhookPostSerializer spec failure --- spec/serializers/web_hook_post_serializer_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/serializers/web_hook_post_serializer_spec.rb b/spec/serializers/web_hook_post_serializer_spec.rb index 2d56df376cd..eabc42be729 100644 --- a/spec/serializers/web_hook_post_serializer_spec.rb +++ b/spec/serializers/web_hook_post_serializer_spec.rb @@ -10,7 +10,7 @@ RSpec.describe WebHookPostSerializer do it 'should only include the required keys' do count = serialized_for_user(admin).keys.count - difference = count - 34 + difference = count - 35 expect(difference).to eq(0), lambda { message = "" From fe62de68ddc3f2db446d4bc123ccea4f5e99e247 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 6 Mar 2019 10:11:31 +0000 Subject: [PATCH 16/67] DEV: Correct sidekiq logging to avoid thread leak --- app/jobs/base.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 515a3c8ab10..648cca60c25 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -67,13 +67,11 @@ module Jobs def self.raw_log(message) @@logger ||= Logger.new("#{Rails.root}/log/sidekiq.log") @@log_queue ||= Queue.new - unless @log_thread&.alive? - @@log_thread = Thread.new do - begin - loop { @@logger << @@log_queue.pop } - rescue Exception => e - Discourse.warn_exception(e, message: "Sidekiq logging thread terminated unexpectedly") - end + @@log_thread ||= Thread.new do + begin + loop { @@logger << @@log_queue.pop } + rescue Exception => e + Discourse.warn_exception(e, message: "Sidekiq logging thread terminated unexpectedly") end end @@log_queue.push(message) From 974e7563697e9857f40679cc45b20e33769d530d Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 6 Mar 2019 11:29:45 +0100 Subject: [PATCH 17/67] FIX: bugs in safari where select-kit is not expanding on iOS (#7118) --- app/assets/stylesheets/common/select-kit/select-kit.scss | 2 -- app/assets/stylesheets/mobile/modal.scss | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/stylesheets/common/select-kit/select-kit.scss b/app/assets/stylesheets/common/select-kit/select-kit.scss index 87497e62c86..c0d86c737b5 100644 --- a/app/assets/stylesheets/common/select-kit/select-kit.scss +++ b/app/assets/stylesheets/common/select-kit/select-kit.scss @@ -24,8 +24,6 @@ z-index: z("dropdown"); .select-kit-body { - -webkit-animation: fadein 0.25s; - animation: fadein 0.25s; display: flex; flex-direction: column; left: 0; diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index 7928653602a..caa03554998 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -66,7 +66,7 @@ form { margin-top: 20px; - input[type="text"] { + input:not(.filter-input)[type="text"] { box-sizing: border-box; width: 100%; } From 0a9a11094dbed101b97759816c1e8a40cec92a54 Mon Sep 17 00:00:00 2001 From: Tarek Khalil <45508821+khalilovcmded@users.noreply.github.com> Date: Wed, 6 Mar 2019 11:21:58 +0000 Subject: [PATCH 18/67] FEATURE: Save ignored usernames in user preferences (#7117) * FEATURE: Save ignored usernames in user preferences --- .../preferences/notifications.js.es6 | 1 + .../javascripts/discourse/models/user.js.es6 | 1 + app/controllers/users_controller.rb | 1 + app/services/user_updater.rb | 32 +++++++++++++++---- spec/services/user_updater_spec.rb | 20 ++++++++++++ 5 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 index de4f84143b8..c680a311cfa 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/notifications.js.es6 @@ -5,6 +5,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error"; export default Ember.Controller.extend(PreferencesTabController, { saveAttrNames: [ "muted_usernames", + "ignored_usernames", "new_topic_duration_minutes", "auto_track_topics_after_msecs", "notification_level_when_replying", diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 18963d737cb..2e5fa5d2f7f 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -249,6 +249,7 @@ const User = RestModel.extend({ "custom_fields", "user_fields", "muted_usernames", + "ignored_usernames", "profile_background", "card_background", "muted_tags", diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index de1032355d4..9be7ef9b809 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1226,6 +1226,7 @@ class UsersController < ApplicationController :title, :date_of_birth, :muted_usernames, + :ignored_usernames, :theme_ids, :locale, :bio_raw, diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 52df88fdab2..261634a6f5e 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -128,6 +128,10 @@ class UserUpdater update_muted_users(attributes[:muted_usernames]) end + if attributes.key?(:ignored_usernames) + update_ignored_users(attributes[:ignored_usernames]) + end + name_changed = user.name_changed? if (saved = (!save_options || user.user_option.save) && user_profile.save && user.save) && (name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0) @@ -157,13 +161,27 @@ class UserUpdater INSERT into muted_users(user_id, muted_user_id, created_at, updated_at) SELECT :user_id, id, :now, :now FROM users - WHERE - id in (:desired_ids) AND - id NOT IN ( - SELECT muted_user_id - FROM muted_users - WHERE user_id = :user_id - ) + WHERE id in (:desired_ids) + ON CONFLICT DO NOTHING + SQL + end + end + + def update_ignored_users(usernames) + usernames ||= "" + desired_ids = User.where(username: usernames.split(",")).pluck(:id) + if desired_ids.empty? + IgnoredUser.where(user_id: user.id).destroy_all + else + IgnoredUser.where('user_id = ? AND ignored_user_id not in (?)', user.id, desired_ids).destroy_all + + # SQL is easier here than figuring out how to do the same in AR + DB.exec(<<~SQL, now: Time.now, user_id: user.id, desired_ids: desired_ids) + INSERT into ignored_users(user_id, ignored_user_id, created_at, updated_at) + SELECT :user_id, id, :now, :now + FROM users + WHERE id in (:desired_ids) + ON CONFLICT DO NOTHING SQL end end diff --git a/spec/services/user_updater_spec.rb b/spec/services/user_updater_spec.rb index 609099ad4af..d4a10da977b 100644 --- a/spec/services/user_updater_spec.rb +++ b/spec/services/user_updater_spec.rb @@ -22,7 +22,27 @@ describe UserUpdater do expect(MutedUser.where(user_id: u2.id).count).to eq 2 expect(MutedUser.where(user_id: u1.id).count).to eq 2 expect(MutedUser.where(user_id: u3.id).count).to eq 0 + end + end + describe '#update_ignored_users' do + it 'updates ignored users' do + u1 = Fabricate(:user) + u2 = Fabricate(:user) + u3 = Fabricate(:user) + + updater = UserUpdater.new(u1, u1) + updater.update_ignored_users("#{u2.username},#{u3.username}") + + updater = UserUpdater.new(u2, u2) + updater.update_ignored_users("#{u3.username},#{u1.username}") + + updater = UserUpdater.new(u3, u3) + updater.update_ignored_users("") + + expect(IgnoredUser.where(user_id: u2.id).count).to eq 2 + expect(IgnoredUser.where(user_id: u1.id).count).to eq 2 + expect(IgnoredUser.where(user_id: u3.id).count).to eq 0 end end From 3ff0800e5093c698babe83f47dec7378df374186 Mon Sep 17 00:00:00 2001 From: Tim Lange Date: Wed, 6 Mar 2019 12:49:17 +0100 Subject: [PATCH 19/67] =?UTF-8?q?FIX:=20Fixed=20custom=20emoji=20circumven?= =?UTF-8?q?ting=20"max=20emojis=20in=20topic=20title"=20set=E2=80=A6=20(#7?= =?UTF-8?q?116)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pretty_text.rb | 3 ++- spec/components/validators/max_emojis_validator_spec.rb | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index e3a89fe4611..d4d8ce56f87 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -230,10 +230,11 @@ module PrettyText return title unless SiteSetting.enable_emoji? set = SiteSetting.emoji_set.inspect + custom = Emoji.custom.map { |e| [e.name, e.url] }.to_h.to_json protect do v8.eval(<<~JS) __paths = #{paths_json}; - __performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set} }); + __performEmojiUnescape(#{title.inspect}, { getURL: __getURL, emojiSet: #{set}, customEmoji: #{custom} }); JS end end diff --git a/spec/components/validators/max_emojis_validator_spec.rb b/spec/components/validators/max_emojis_validator_spec.rb index 671caaea999..1242a32a4e8 100644 --- a/spec/components/validators/max_emojis_validator_spec.rb +++ b/spec/components/validators/max_emojis_validator_spec.rb @@ -14,7 +14,9 @@ describe MaxEmojisValidator do shared_examples "validating any topic title" do it 'adds an error when emoji count is greater than SiteSetting.max_emojis_in_title' do SiteSetting.max_emojis_in_title = 3 - record.title = '🧐 Lots of emojis here 🎃 :joy: :)' + CustomEmoji.create!(name: 'trout', upload: Fabricate(:upload)) + Emoji.clear_cache + record.title = '🧐 Lots of emojis here 🎃 :trout: :)' validate expect(record.errors[:title][0]).to eq(I18n.t("errors.messages.max_emojis", max_emojis_count: 3)) From df474bceee6a657195896539e8baff51e43358a0 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 6 Mar 2019 12:50:15 +0000 Subject: [PATCH 20/67] DEV: Further sidekiq logging stability improvements - Open the log file in "append" mode. This avoids issues if the file does not exist (and matches standard rails log behavior) - Correctly parse the interval logging environment variable --- app/jobs/base.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 648cca60c25..aec6fd86de9 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -65,7 +65,11 @@ module Jobs end def self.raw_log(message) - @@logger ||= Logger.new("#{Rails.root}/log/sidekiq.log") + @@logger ||= begin + f = File.open "#{Rails.root}/log/sidekiq.log", "a" + f.sync = true + Logger.new f + end @@log_queue ||= Queue.new @@log_thread ||= Thread.new do begin @@ -99,7 +103,7 @@ module Jobs begin loop do sleep interval.to_i - @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval } + @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval.to_i } end rescue Exception => e Discourse.warn_exception(e, message: "Sidekiq interval logging thread terminated unexpectedly") From 6d2708c3fb1e59db762dd85aa33b4e21061b7017 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Wed, 6 Mar 2019 09:21:31 -0500 Subject: [PATCH 21/67] Bump FontAwesome icons to 5.7.2 --- lib/svg_sprite/svg_sprite.rb | 4 +- package.json | 2 +- .../assets/svg-icons/fontawesome/brands.svg | 234 ++++++++---- .../assets/svg-icons/fontawesome/regular.svg | 20 +- vendor/assets/svg-icons/fontawesome/solid.svg | 358 +++++++++++++++--- yarn.lock | 8 +- 6 files changed, 489 insertions(+), 137 deletions(-) diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index df3259fdbd1..7037c7a0e54 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -224,10 +224,10 @@ module SvgSprite icons = all_icons(theme_ids) doc = File.open("#{Rails.root}/vendor/assets/svg-icons/fontawesome/solid.svg") { |f| Nokogiri::XML(f) } - fa_license = doc.at('//comment()').text svg_subset = """ """.dup diff --git a/package.json b/package.json index 65923e79e74..2e05f64c6b0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "author": "Discourse", "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-free": "5.5.0", + "@fortawesome/fontawesome-free": "5.7.2", "ace-builds": "1.4.2", "bootbox": "3.2.0", "chart.js": "2.7.3", diff --git a/vendor/assets/svg-icons/fontawesome/brands.svg b/vendor/assets/svg-icons/fontawesome/brands.svg index ecf73b20ed6..1c46f9a9a31 100644 --- a/vendor/assets/svg-icons/fontawesome/brands.svg +++ b/vendor/assets/svg-icons/fontawesome/brands.svg @@ -1,8 +1,4 @@ - 500px @@ -14,16 +10,20 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Accusoft - + - + Acquisitions Incorporated - + App.net + + Adobe + + Adversal @@ -50,7 +50,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Amilia - + Android @@ -88,10 +88,18 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Apple Pay + + Artstation + + Asymmetrik, Ltd. + + Atlassian + + Audible @@ -128,9 +136,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL BIMobject - + Bitbucket - + Bitcoin @@ -176,13 +184,17 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL BuySellAds + + Canadian Maple Leaf + + Amazon Pay Credit Card American Express Credit Card - + Apple Pay Credit Card @@ -194,7 +206,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Discover Credit Card - + JCB Credit Card @@ -210,7 +222,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Stripe Credit Card - + Visa Credit Card @@ -220,6 +232,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Centercode + + Centos + + Chrome @@ -244,6 +260,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Codie Pie + + Confluence + + Connect Develop @@ -254,7 +274,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL cPanel - + Creative Commons @@ -285,8 +305,8 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL - Creative Commons Public Domain Alternate - + Alternate Creative Commons Public Domain + Creative Commons Remix @@ -334,15 +354,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL D&D Beyond - + DashCube - Delicious Logo - + Delicious + deploy.dog @@ -360,13 +380,21 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL deviantART + + DHL + + + + Diaspora + + Digg Logo Digital Ocean - + Discord @@ -386,7 +414,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Draft2digital - + Dribbble @@ -414,7 +442,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL eBay - + Edge Browser @@ -430,7 +458,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Ember - + Galactic Empire @@ -442,7 +470,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Erlang - + Ethereum @@ -462,7 +490,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Facebook F - + Facebook Messenger @@ -476,9 +504,21 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Fantasy Flight-games + + FedEx + + + + Fedora + + + + Figma + + Firefox - + First Order @@ -510,7 +550,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Font Awesome - + Font Awesome Flag @@ -522,7 +562,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Fonticons - + Fonticons Fi @@ -534,7 +574,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Fort Awesome - + Forumbee @@ -602,7 +642,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL GitLab - + Gitter @@ -666,11 +706,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Gripfire, Inc. - + Grunt - + Gulp @@ -690,7 +730,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hips - + HireAHelper @@ -698,7 +738,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hooli - + Hornbill @@ -722,16 +762,24 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL IMDB - + Instagram + + Intercom + + Internet-explorer + + InVision + + ioxhost @@ -746,19 +794,23 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Java - + Jedi Order - + Jenkis + + Jira + + Joget - + Joomla Logo @@ -776,9 +828,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL jsFiddle - + Kaggle - + Keybase @@ -838,7 +890,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Linux - + lyft @@ -848,9 +900,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Magento - + Mailchimp - + Mandalorian @@ -858,7 +910,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Markdown - + Mastodon @@ -890,7 +942,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Megaport - + + + + Mendeley + Microsoft @@ -978,7 +1034,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Optin Monster - + Open Source Initiative @@ -1034,7 +1090,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Pied Piper Logo - + Pied Piper-hat @@ -1070,7 +1126,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Python - + QQ @@ -1078,7 +1134,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL QuinScape - + Quora @@ -1088,17 +1144,21 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL R Project + + Raspberry Pi + + Ravelry React - + ReactEurope - + ReadMe @@ -1124,6 +1184,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL reddit Square + + Redhat + + Renren @@ -1158,7 +1222,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sass - + SCHLIX @@ -1186,7 +1250,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Shirts in Bulk - + Shopware @@ -1204,6 +1268,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sith + + Sketch + + skyatlas @@ -1214,7 +1282,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Slack Logo - + Slack Hashtag @@ -1222,7 +1290,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Slideshare - + Snapchat @@ -1240,6 +1308,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL SoundCloud + + Sourcetree + + Speakap @@ -1258,11 +1330,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Stack Overflow - + StayLinked - + Steam @@ -1278,19 +1350,19 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sticker Mule - + Strava - + Stripe - + Stripe S - + Studio Vinari @@ -1312,9 +1384,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Supple + + Suse + + TeamSpeak - + Telegram @@ -1328,9 +1404,9 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Tencent Weibo - + The Red Yeti - + Themeco @@ -1342,15 +1418,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Think Peaks - + Trade Federation - + Trello - + TripAdvisor @@ -1378,28 +1454,40 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Typo3 - + Uber + + Ubuntu + + UIkit Uniregistry - + Untappd + + UPS + + USB + + United States Postal Service + + us-Sunnah Foundation @@ -1548,9 +1636,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Yandex International - + + Yarn + + + Yelp - + Yoast diff --git a/vendor/assets/svg-icons/fontawesome/regular.svg b/vendor/assets/svg-icons/fontawesome/regular.svg index 8a77d05c68b..c9b33b62427 100644 --- a/vendor/assets/svg-icons/fontawesome/regular.svg +++ b/vendor/assets/svg-icons/fontawesome/regular.svg @@ -1,8 +1,4 @@ - Address Book @@ -106,7 +102,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Clipboard - + Clock @@ -174,11 +170,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Eye - + - + Eye Slash - + File @@ -190,7 +186,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Archive File - + Audio File @@ -198,7 +194,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Code File - + Excel File @@ -238,7 +234,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Folder Open - + Font Awesome Full Logo @@ -586,7 +582,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Trash - + User diff --git a/vendor/assets/svg-icons/fontawesome/solid.svg b/vendor/assets/svg-icons/fontawesome/solid.svg index 4654dee7e14..aa652f5d0c2 100644 --- a/vendor/assets/svg-icons/fontawesome/solid.svg +++ b/vendor/assets/svg-icons/fontawesome/solid.svg @@ -1,8 +1,4 @@ - + + Baby + + + + Baby Carriage + + Backspace @@ -204,6 +208,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL backward + + Bacon + + Balance Scale @@ -288,6 +296,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Binoculars + + Biohazard + + Birthday Cake @@ -304,6 +316,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Blind + + Blog + + bold @@ -330,7 +346,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Book of the Dead - + + + + Medical Book + Book Open @@ -354,7 +374,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Box Open - + Boxes @@ -368,6 +388,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Brain + + Bread Slice + + Briefcase @@ -430,12 +454,16 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Calendar - + Calendar Check + + Calendar with Day Focus + + Calendar Minus @@ -448,6 +476,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Calendar Times + + Calendar with Week Focus + + camera @@ -460,6 +492,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Campground + + Candy Cane + + Cannabis @@ -520,6 +556,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Caret Up + + Carrot + + Shopping Cart Arrow Down @@ -528,6 +568,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Add to Shopping Cart + + Cash Register + + Cat @@ -538,7 +582,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Chair - + Chalkboard @@ -584,9 +628,13 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Check Square + + Cheese + + Chess - + Chess Bishop @@ -594,7 +642,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Chess Board - + Chess King @@ -668,6 +716,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL City + + Medical Clinic + + Clipboard @@ -750,7 +802,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL cog - + cogs @@ -780,6 +832,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Comment Dots + + Alternate Medical Chat + + Comment Slash @@ -804,6 +860,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Compress + + Alternate Compress Arrows + + Concierge Bell @@ -856,6 +916,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Crown + + Crutch + + Cube @@ -1010,12 +1074,20 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Drumstick with Bite Taken Out - + Dumbbell + + Dumpster + + + + Dumpster Fire + + Dungeon @@ -1024,6 +1096,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Edit + + Egg + + eject @@ -1060,6 +1136,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL eraser + + Ethernet + + Euro Sign @@ -1098,15 +1178,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Eye - + Eye Dropper - + Eye Slash - + fast-backward @@ -1146,7 +1226,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Archive File - + Audio File @@ -1174,7 +1254,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL File Export - + Image File @@ -1182,7 +1262,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL File Import - + File Invoice @@ -1250,7 +1330,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL fire - + + + + Alternate Fire + fire-extinguisher @@ -1274,7 +1358,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL flag-checkered - + United States of America Flag @@ -1306,7 +1390,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL font - + Font Awesome Full Logo @@ -1366,7 +1450,15 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL gift - + + + + Gifts + + + + Glass Cheers + Martini Glass @@ -1376,6 +1468,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Glass Martini + + Glass Whiskey + + Glasses @@ -1396,13 +1492,17 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Globe with Asia shown + + Globe with Europe shown + + Golf Ball Gopuram - + Graduation Cap @@ -1476,14 +1576,30 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Grip Horizontal + + Grip Lines + + + + Grip Lines Vertical + + Grip Vertical + + Guitar + + H Square + + Hamburger + + Hammer @@ -1508,6 +1624,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Lizard (Hand) + + Hand with Middle Finger Raised + + Paper (Hand) @@ -1538,7 +1658,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Rock (Hand) - + Scissors (Hand) @@ -1546,7 +1666,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Spock (Hand) - + Hands @@ -1564,6 +1684,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hanukiah + + Hard Hat + + Hashtag @@ -1600,6 +1724,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Heart + + Heart Broken + + Heartbeat @@ -1628,14 +1756,22 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hockey Puck + + Holly Berry + + home - + Horse + + Horse Head + + hospital @@ -1652,6 +1788,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Hot Tub + + Hot Dog + + Hotel @@ -1684,6 +1824,14 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL I Beam Cursor + + Ice Cream + + + + Icicles + + Identification Badge @@ -1696,6 +1844,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Identification Card + + Igloo + + Image @@ -1782,7 +1934,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Language - + Laptop @@ -1792,6 +1944,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Laptop Code + + Laptop Medical + + Grinning Face With Big Eyes @@ -2048,6 +2204,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Minus Square + + Mitten + + Mobile Phone @@ -2098,7 +2258,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Motorcycle - + Mountain @@ -2108,6 +2268,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Mouse Pointer + + Mug Hot + + Music @@ -2156,6 +2320,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Outdent + + Pager + + Paint Brush @@ -2252,6 +2420,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL People Carry + + Hot Pepper + + Percent @@ -2288,8 +2460,12 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Pills + + Pizza Slice + + - Place Of Worship + Place of Worship @@ -2428,6 +2604,14 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Quran + + Radiation + + + + Alternate Radiation + + Rainbow @@ -2468,6 +2652,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Republican + + Restroom + + Retweet @@ -2490,7 +2678,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL rocket - + Route @@ -2540,6 +2728,14 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Loudly Crying Face + + Satellite + + + + Satellite Dish + + Save @@ -2556,6 +2752,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Scroll + + Sd Card + + Search @@ -2610,7 +2810,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Alternate Shield - + Ship @@ -2664,14 +2864,30 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL signal - + Signature - + + + + SIM Card + Sitemap + + Skating + + + + Skiing + + + + Skiing Nordic + + Skull @@ -2684,6 +2900,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Slash + + Sleigh + + Horizontal Sliders @@ -2712,10 +2932,26 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Smoking Ban + + SMS + + + + Snowboarding + + Snowflake + + Snowman + + + + Snowplow + + Socks @@ -2730,11 +2966,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Sort Alpha Down - + Sort Alpha Up - + Sort Amount Down @@ -2793,7 +3029,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL - Square Root Alternate + Alternate Square Root @@ -2878,7 +3114,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL subscript - + Subway @@ -2898,7 +3134,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL superscript - + Hushed Face @@ -2992,6 +3228,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Low Temperature + + Tenge + + Terminal @@ -3086,7 +3326,11 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Toggle On - + + + + Toilet + Toilet Paper @@ -3096,13 +3340,17 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Toolbox + + Tools + + Tooth - + Torah - + Torii Gate @@ -3114,7 +3362,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Trademark - + Traffic Light @@ -3124,6 +3372,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Train + + Tram + + Transgender @@ -3134,11 +3386,19 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Trash - + Alternate Trash - + + + + Trash Restore + + + + Alternative Trash Restore + Tree @@ -3277,7 +3537,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL - user-md + Doctor @@ -3288,6 +3548,10 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL User Ninja + + Nurse + + User Plus @@ -3462,7 +3726,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Won Sign - + Wrench @@ -3474,7 +3738,7 @@ License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL Yen Sign - + Yin Yang diff --git a/yarn.lock b/yarn.lock index 4aac87712db..8e7bf1979b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -87,10 +87,10 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@fortawesome/fontawesome-free@5.5.0": - version "5.5.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.5.0.tgz#0c6c53823d04457ae669cd19567b8a21dbb4fcfd" - integrity sha512-p4lu0jfj5QN013ddArh99r3OXZ/fp9rbovs62LfaO70OMBsAXxtNd0lAq/97fitrscR0fqfd+/a5KNcp6Sh/0A== +"@fortawesome/fontawesome-free@5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.7.2.tgz#1498c3eb78ee7c78c5488418707de90aaf58d5d7" + integrity sha512-Ha4HshKdCVKgu4TVCtG8XyPPYdzTzNW4/fvPnn+LT7AosRABryhlRv4cc4+o84dgpvVJN9reN7jo/c+nYujFug== "@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.3.0": version "1.3.0" From 5cf63e43c6d0bcf604cccc9d967cf393afc1bbcd Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 10:26:34 -0500 Subject: [PATCH 22/67] FEATURE: Add about-after-description plugin outlet --- app/assets/javascripts/discourse/templates/about.hbs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/discourse/templates/about.hbs b/app/assets/javascripts/discourse/templates/about.hbs index fb732e1700b..a0ef47a5c99 100644 --- a/app/assets/javascripts/discourse/templates/about.hbs +++ b/app/assets/javascripts/discourse/templates/about.hbs @@ -19,6 +19,11 @@

{{model.description}}

+ {{plugin-outlet name="about-after-description" + connectorTagName='section' + tagName='' + args=(hash model=model)}} + {{#if model.admins}}

{{d-icon "users"}} {{i18n 'about.our_admins'}}

From 40009784522604b577b88bce633fdc5eb4848598 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 6 Mar 2019 16:49:40 +0100 Subject: [PATCH 23/67] FIX: Failed to save email template with pluralized subject --- .../admin/email_templates_controller.rb | 7 +++++-- .../admin/email_templates_controller_spec.rb | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index f36fa25a16e..aa9eef412f7 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -113,12 +113,15 @@ class Admin::EmailTemplatesController < Admin::AdminController def update_key(key, value) old_value = I18n.t(key) - translation_override = TranslationOverride.upsert!(I18n.locale, key, value) + + unless old_value.is_a?(Hash) + translation_override = TranslationOverride.upsert!(I18n.locale, key, value) + end { key: key, old_value: old_value, - error_messages: translation_override.errors.full_messages + error_messages: translation_override&.errors&.full_messages } end diff --git a/spec/requests/admin/email_templates_controller_spec.rb b/spec/requests/admin/email_templates_controller_spec.rb index 150112d3541..b0d3a0a0550 100644 --- a/spec/requests/admin/email_templates_controller_spec.rb +++ b/spec/requests/admin/email_templates_controller_spec.rb @@ -214,6 +214,21 @@ RSpec.describe Admin::EmailTemplatesController do end end + context "when subject has plural keys" do + it "doesn't update the subject" do + old_subject = I18n.t('system_messages.pending_users_reminder.subject_template') + expect(old_subject).to be_a(Hash) + + put '/admin/customize/email_templates/system_messages.pending_users_reminder', params: { + email_template: { subject: '', body: 'Lorem ipsum' } + }, headers: headers + + expect(response.status).to eq(200) + + expect(I18n.t('system_messages.pending_users_reminder.subject_template')).to eq(old_subject) + expect(I18n.t('system_messages.pending_users_reminder.text_body_template')).to eq('Lorem ipsum') + end + end end end From 2db365233825067a2165d1d85c18806134ac25f6 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 11:05:11 -0500 Subject: [PATCH 24/67] UX: Fix redeemed invite table layout --- .../javascripts/discourse/templates/user-invited-show.hbs | 5 ++--- app/assets/stylesheets/desktop/user.scss | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index f04959ea92a..5b4c000b3b5 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -83,8 +83,7 @@ {{format-date invite.created_at}} {{#if invite.expired}} - {{i18n 'user.invited.expired'}} -      +
{{i18n 'user.invited.expired'}}
{{/if}} {{#if invite.rescinded}} {{i18n 'user.invited.rescinded'}} @@ -94,7 +93,7 @@ {{#if invite.reinvited}} - {{i18n 'user.invited.reinvited'}} +
{{i18n 'user.invited.reinvited'}}
{{else}} {{d-button icon="sync" action=(action "reinvite") actionParam=invite label="user.invited.reinvite"}} {{/if}} diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 7c5cfaee3c0..e03a940bc6d 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -119,9 +119,6 @@ td { padding: 0.667em; } - td:nth-of-type(n + 3) { - text-align: right; - } } } From 167d85c21fd1d0ecab206c4cb45384bae41f259c Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 6 Mar 2019 22:52:54 +0530 Subject: [PATCH 25/67] FIX: post & topic destroyed hooks not triggering with tag filter --- app/models/web_hook.rb | 12 +++++++----- lib/post_destroyer.rb | 12 ++---------- spec/models/web_hook_spec.rb | 27 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index b2fe649156c..5d46756ad47 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -65,10 +65,12 @@ class WebHook < ActiveRecord::Base end end - def self.enqueue_topic_hooks(event, topic) + def self.enqueue_topic_hooks(event, topic, payload = nil) if active_web_hooks('topic').exists? && topic.present? - topic_view = TopicView.new(topic.id, Discourse.system_user) - payload = WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer) + payload ||= begin + topic_view = TopicView.new(topic.id, Discourse.system_user) + WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer) + end WebHook.enqueue_hooks(:topic, event, id: topic.id, @@ -79,9 +81,9 @@ class WebHook < ActiveRecord::Base end end - def self.enqueue_post_hooks(event, post) + def self.enqueue_post_hooks(event, post, payload = nil) if active_web_hooks('post').exists? && post.present? - payload = WebHook.generate_payload(:post, post) + payload ||= WebHook.generate_payload(:post, post) WebHook.enqueue_hooks(:post, event, id: post.id, diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 54e4451fbfe..16e91f4c586 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -61,19 +61,11 @@ class PostDestroyer mark_for_deletion(delete_removed_posts_after) end DiscourseEvent.trigger(:post_destroyed, @post, @opts, @user) - WebHook.enqueue_hooks(:post, :post_destroyed, - id: @post.id, - category_id: @post&.topic&.category_id, - payload: payload - ) if WebHook.active_web_hooks(:post).exists? + WebHook.enqueue_post_hooks(:post_destroyed, @post, payload) if @post.is_first_post? && @post.topic DiscourseEvent.trigger(:topic_destroyed, @post.topic, @user) - WebHook.enqueue_hooks(:topic, :topic_destroyed, - id: topic.id, - category_id: topic&.category_id, - payload: topic_payload - ) if WebHook.active_web_hooks(:topic).exists? + WebHook.enqueue_topic_hooks(:topic_destroyed, @post.topic, topic_payload) end end diff --git a/spec/models/web_hook_spec.rb b/spec/models/web_hook_spec.rb index 89beb7042d0..3156c35777f 100644 --- a/spec/models/web_hook_spec.rb +++ b/spec/models/web_hook_spec.rb @@ -257,6 +257,33 @@ describe WebHook do expect(payload["id"]).to eq(post.topic.id) end + it 'should enqueue the destroyed hooks with tag filter for post events' do + tag = Fabricate(:tag) + Fabricate(:web_hook, tags: [tag]) + + post = PostCreator.create!(user, + raw: 'post', + topic_id: topic.id, + reply_to_post_number: 1, + skip_validations: true + ) + + topic.tags = [tag] + topic.save! + + Jobs::EmitWebHookEvent.jobs.clear + PostDestroyer.new(user, post).destroy + + job = Jobs::EmitWebHookEvent.new + job.expects(:web_hook_request).times(2) + + args = Jobs::EmitWebHookEvent.jobs[1]["args"].first + job.execute(args.with_indifferent_access) + + args = Jobs::EmitWebHookEvent.jobs[2]["args"].first + job.execute(args.with_indifferent_access) + end + it 'should enqueue the right hooks for user events' do Fabricate(:user_web_hook, active: true) From b1035cc691705e3afb60bd53d80a7030757164f4 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Wed, 6 Mar 2019 12:30:36 -0500 Subject: [PATCH 26/67] FIX: NodeBB import details - mark imported users as active - do not strip @ from usernames in post content - improve uploads path matching --- script/import_scripts/nodebb/nodebb.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/script/import_scripts/nodebb/nodebb.rb b/script/import_scripts/nodebb/nodebb.rb index 3e5c616a5d6..45078de4239 100644 --- a/script/import_scripts/nodebb/nodebb.rb +++ b/script/import_scripts/nodebb/nodebb.rb @@ -144,6 +144,7 @@ class ImportScripts::NodeBB < ImportScripts::Base suspended_till: suspended_till, primary_group_id: group_id_from_imported_group_id(user["groupTitle"]), created_at: user["joindate"], + active: true, custom_fields: { import_pass: user["password"] }, @@ -197,13 +198,14 @@ class ImportScripts::NodeBB < ImportScripts::Base upload = UploadCreator.new(file, filename).create_for(imported_user.id) else - # remove "/assets/uploads/" from attachment + # remove "/assets/uploads/" and "/uploads" from attachment picture = picture.gsub("/assets/uploads", "") + picture = picture.gsub("/uploads", "") filepath = File.join(ATTACHMENT_DIR, picture) filename = File.basename(picture) unless File.exists?(filepath) - puts "Avatar file doesn't exist: #{filename}" + puts "Avatar file doesn't exist: #{filepath}" return nil end @@ -256,13 +258,14 @@ class ImportScripts::NodeBB < ImportScripts::Base upload = UploadCreator.new(file, filename).create_for(imported_user.id) else - # remove "/assets/uploads/" from attachment + # remove "/assets/uploads/" and "/uploads" from attachment picture = picture.gsub("/assets/uploads", "") + picture = picture.gsub("/uploads", "") filepath = File.join(ATTACHMENT_DIR, picture) filename = File.basename(picture) unless File.exists?(filepath) - puts "Background file doesn't exist: #{filename}" + puts "Background file doesn't exist: #{filepath}" return nil end @@ -509,13 +512,6 @@ class ImportScripts::NodeBB < ImportScripts::Base end end - # @username with dash to underscore - raw = raw.gsub(/@([a-zA-Z0-9-]+)/) do - username = $1 - - username.gsub('-', '_') - end - raw end end From 655a08dbbda1636409b15cc5611fcd3afc63263d Mon Sep 17 00:00:00 2001 From: maulkin Date: Wed, 6 Mar 2019 20:29:37 +0000 Subject: [PATCH 27/67] FIX: Return actual errors if PostCreator fails (#7096) --- script/import_scripts/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index b6bb1762cec..4b2965e765b 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -563,7 +563,7 @@ class ImportScripts::Base post_creator = PostCreator.new(user, opts) post = post_creator.create post_create_action.try(:call, post) if post - post ? post : post_creator.errors.full_messages + if !post || post_creator.errors&.length > 0 ? post : post_creator.errors.full_messages end def create_upload(user_id, path, source_filename) From 5ba2ef42743bbe2ad78ad016dc0f8d7acf2b2d94 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Thu, 7 Mar 2019 02:17:29 +0530 Subject: [PATCH 28/67] UX: Display error message if no time frame selected in topic status update --- .../discourse/controllers/edit-topic-timer.js.es6 | 8 ++++++++ config/locales/client.en.yml | 1 + 2 files changed, 9 insertions(+) diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 index 07d6cb83fc3..b710c5feb5d 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-timer.js.es6 @@ -103,6 +103,14 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { saveTimer() { + if (!this.get("topicTimer.updateTime")) { + this.flash( + I18n.t("topic.topic_status_update.time_frame_required"), + "alert-error" + ); + return; + } + this._setTimer( this.get("topicTimer.updateTime"), this.get("topicTimer.status_type") diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e2dfc29843d..5ae7ddc8626 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1809,6 +1809,7 @@ en: when: "When:" public_timer_types: Topic Timers private_timer_types: User Topic Timers + time_frame_required: Please select a time frame auto_update_input: none: "Select a timeframe" later_today: "Later today" From 1121514799014fca17622265fc479de7e302c8a0 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 6 Mar 2019 20:24:10 +0100 Subject: [PATCH 29/67] UX: Localize date format in "new user of the month" message --- .../scheduled/grant_new_user_of_the_month_badges.rb | 2 +- config/locales/server.en.yml | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb index d6a4a16d56c..a14db0bbadc 100644 --- a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb +++ b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb @@ -24,7 +24,7 @@ module Jobs BadgeGranter.grant(badge, user) SystemMessage.new(user).create('new_user_of_the_month', - month_year: Time.now.strftime("%B %Y"), + month_year: I18n.l(Time.now, format: :no_day), url: "#{Discourse.base_url}/badges" ) end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e578529144f..f457a08e08a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -25,14 +25,16 @@ en: datetime_formats: &datetime_formats formats: - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime short: "%m-%d-%Y" - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime short_no_year: "%B %-d" - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime date_only: "%B %-d, %Y" - # Format directives: https://ruby-doc.org/core-2.3.1/Time.html#method-i-strftime + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime long: "%B %-d, %Y, %l:%M%P" + # Format directives: https://ruby-doc.org/core-2.6.1/Time.html#method-i-strftime + no_day: "%B %Y" date: # Do not remove the brackets and commas and do not translate the first month name. It should be "null". month_names: From 941e096df4b88943079f751082c3708da5da449b Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 6 Mar 2019 21:58:06 +0100 Subject: [PATCH 30/67] Fix error in base import script Follow-up to 655a08dbbda1636409b15cc5611fcd3afc63263d --- script/import_scripts/base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index 4b2965e765b..39d20568467 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -563,7 +563,7 @@ class ImportScripts::Base post_creator = PostCreator.new(user, opts) post = post_creator.create post_create_action.try(:call, post) if post - if !post || post_creator.errors&.length > 0 ? post : post_creator.errors.full_messages + post && post_creator.errors.empty? ? post : post_creator.errors.full_messages end def create_upload(user_id, path, source_filename) From 078c02cd245a3906b41967715c2630eca557c0c6 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 16:09:37 -0500 Subject: [PATCH 31/67] UX: More consistent structure for active nav-pill --- .../javascripts/discourse/components/edit-category-tab.js.es6 | 2 +- .../discourse/templates/components/edit-category-tab.hbs | 2 +- app/assets/stylesheets/common/components/navs.scss | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 index 7c2509f0680..87eed4459ca 100644 --- a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 @@ -2,7 +2,7 @@ import { propertyEqual } from "discourse/lib/computed"; export default Ember.Component.extend({ tagName: "li", - classNameBindings: ["active", "tabClassName"], + classNameBindings: ["tabClassName"], tabClassName: function() { return "edit-category-" + this.get("tab"); diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs index 4aeb37702a1..b6108dfd6ad 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs @@ -1 +1 @@ -{{title}} + {{title}} diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 75b797e261b..12eb49b4ce0 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -48,7 +48,6 @@ } } - &.active > a, > a.active { color: $secondary; background-color: $quaternary; From 733deba5850176d861e0c68783a9f021c3a19aff Mon Sep 17 00:00:00 2001 From: Bianca Nenciu Date: Wed, 6 Mar 2019 23:14:55 +0200 Subject: [PATCH 32/67] FIX: Loading draft also replaces the recipients (#7122) --- .../discourse/controllers/composer.js.es6 | 2 +- .../acceptance/composer-test.js.es6 | 18 + .../javascripts/fixtures/user_fixtures.js.es6 | 329 ++++++++++++++++++ 3 files changed, 348 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 8156df8e42b..52deb54fb28 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -902,7 +902,7 @@ export default Ember.Controller.extend({ composerModel.set("composeState", Composer.OPEN); composerModel.set("isWarning", false); - if (opts.usernames) { + if (opts.usernames && !this.get("model.targetUsernames")) { this.set("model.targetUsernames", opts.usernames); } diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 index 9ad9d448ccc..fcbff948dc7 100644 --- a/test/javascripts/acceptance/composer-test.js.es6 +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -600,6 +600,24 @@ QUnit.test("Checks for existing draft", async assert => { toggleCheckDraftPopup(false); }); +QUnit.test("Loading draft also replaces the recipients", async assert => { + toggleCheckDraftPopup(true); + + // prettier-ignore + server.get("/draft.json", () => { // eslint-disable-line no-undef + return [ 200, { "Content-Type": "application/json" }, { + "draft":"{\"reply\":\"hello\",\"action\":\"privateMessage\",\"title\":\"hello\",\"categoryId\":null,\"archetypeId\":\"private_message\",\"metaData\":null,\"usernames\":\"codinghorror\",\"composerTime\":9159,\"typingTime\":2500}", + "draft_sequence":0 + } ]; + }); + + await visit("/u/charlie"); + await click("button.compose-pm"); + await click(".modal .btn-default"); + + assert.equal(find(".users-input .item:eq(0)").text(), "codinghorror"); +}); + const assertImageResized = (assert, uploads) => { assert.equal( find(".d-editor-input").val(), diff --git a/test/javascripts/fixtures/user_fixtures.js.es6 b/test/javascripts/fixtures/user_fixtures.js.es6 index 51df0412233..203f3a76410 100644 --- a/test/javascripts/fixtures/user_fixtures.js.es6 +++ b/test/javascripts/fixtures/user_fixtures.js.es6 @@ -2276,5 +2276,334 @@ export default { } ] } + }, + "/u/charlie.json": { + user_badges: [ + { + id: 17, + granted_at: "2019-03-06T19:08:28.230Z", + count: 1, + badge_id: 3, + user_id: 5, + granted_by_id: -1 + } + ], + badges: [ + { + id: 3, + name: "Regular", + description: + '\u003ca href="https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/"\u003eGranted\u003c/a\u003e recategorize, rename, followed links, wiki, more likes', + grant_count: 3, + allow_title: true, + multiple_grant: false, + icon: "fa-user", + image: null, + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + slug: "regular", + manually_grantable: false, + badge_type_id: 2 + } + ], + badge_types: [{ id: 2, name: "Silver", sort_order: 8 }], + users: [ + { + id: 5, + username: "charlie", + name: null, + avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + moderator: false, + admin: false + }, + { + id: -1, + username: "system", + name: "system", + avatar_template: "/user_avatar/localhost/system/{size}/2_2.png", + moderator: true, + admin: true + } + ], + user: { + id: 5, + username: "charlie", + name: null, + avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + last_posted_at: null, + last_seen_at: null, + created_at: "2019-03-06T19:06:20.340Z", + can_edit: true, + can_edit_username: true, + can_edit_email: true, + can_edit_name: true, + ignored: false, + can_ignore_user: false, + can_send_private_messages: true, + can_send_private_message_to_user: true, + trust_level: 3, + moderator: false, + admin: false, + title: null, + uploaded_avatar_id: null, + badge_count: 3, + has_title_badges: true, + custom_fields: {}, + pending_count: 0, + profile_view_count: 1, + time_read: 0, + recent_time_read: 0, + primary_group_name: null, + primary_group_flair_url: null, + primary_group_flair_bg_color: null, + primary_group_flair_color: null, + staged: false, + second_factor_enabled: false, + post_count: 0, + can_be_deleted: true, + can_delete_all_posts: true, + locale: null, + muted_category_ids: [], + watched_tags: [], + watching_first_post_tags: [], + tracked_tags: [], + muted_tags: [], + tracked_category_ids: [], + watched_category_ids: [], + watched_first_post_category_ids: [], + system_avatar_upload_id: null, + system_avatar_template: + "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + muted_usernames: [], + ignored_usernames: [], + mailing_list_posts_per_day: 0, + can_change_bio: true, + user_api_keys: null, + user_auth_tokens: [], + user_auth_token_logs: [], + invited_by: null, + groups: [ + { + id: 10, + automatic: true, + name: "trust_level_0", + display_name: "trust_level_0", + user_count: 14, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + }, + { + id: 11, + automatic: true, + name: "trust_level_1", + display_name: "trust_level_1", + user_count: 9, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + }, + { + id: 12, + automatic: true, + name: "trust_level_2", + display_name: "trust_level_2", + user_count: 6, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + }, + { + id: 13, + automatic: true, + name: "trust_level_3", + display_name: "trust_level_3", + user_count: 3, + mentionable_level: 0, + messageable_level: 0, + visibility_level: 0, + automatic_membership_email_domains: null, + automatic_membership_retroactive: false, + primary_group: false, + title: null, + grant_trust_level: null, + incoming_email: null, + has_messages: false, + flair_url: null, + flair_bg_color: null, + flair_color: null, + bio_raw: null, + bio_cooked: null, + public_admission: false, + public_exit: false, + allow_membership_requests: false, + full_name: null, + default_notification_level: 3, + membership_request_template: null + } + ], + group_users: [ + { group_id: 10, user_id: 5, notification_level: 3 }, + { group_id: 11, user_id: 5, notification_level: 3 }, + { group_id: 12, user_id: 5, notification_level: 3 }, + { group_id: 13, user_id: 5, notification_level: 3 } + ], + featured_user_badge_ids: [17], + user_option: { + user_id: 5, + email_always: false, + mailing_list_mode: false, + mailing_list_mode_frequency: 1, + email_digests: true, + email_private_messages: true, + email_direct: true, + external_links_in_new_tab: false, + dynamic_favicon: false, + enable_quoting: true, + disable_jump_reply: false, + digest_after_minutes: 10080, + automatically_unpin_topics: true, + auto_track_topics_after_msecs: 240000, + notification_level_when_replying: 2, + new_topic_duration_minutes: 2880, + email_previous_replies: 2, + email_in_reply_to: true, + like_notification_frequency: 1, + include_tl0_in_digests: false, + theme_ids: [2], + theme_key_seq: 0, + allow_private_messages: true, + homepage_id: null, + hide_profile_and_presence: false, + text_size: "normal", + text_size_seq: 0 + } + } + }, + "/u/charlie/summary.json": { + topics: [], + badges: [ + { + id: 3, + name: "Regular", + description: + '\u003ca href="https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/"\u003eGranted\u003c/a\u003e recategorize, rename, followed links, wiki, more likes', + grant_count: 3, + allow_title: true, + multiple_grant: false, + icon: "fa-user", + image: null, + listable: true, + enabled: true, + badge_grouping_id: 4, + system: true, + slug: "regular", + manually_grantable: false, + badge_type_id: 2 + } + ], + badge_types: [{ id: 2, name: "Silver", sort_order: 8 }], + users: [ + { + id: 5, + username: "charlie", + name: null, + avatar_template: "/letter_avatar_proxy/v3/letter/c/d6d6ee/{size}.png", + moderator: false, + admin: false + }, + { + id: -1, + username: "system", + name: "system", + avatar_template: "/user_avatar/localhost/system/{size}/2_2.png", + moderator: true, + admin: true + } + ], + user_summary: { + likes_given: 0, + likes_received: 0, + topics_entered: 0, + posts_read_count: 0, + days_visited: 0, + topic_count: 0, + post_count: 0, + time_read: 0, + recent_time_read: 0, + topic_ids: [], + replies: [], + links: [], + most_liked_by_users: [], + most_liked_users: [], + most_replied_to_users: [], + badges: [ + { + id: 17, + granted_at: "2019-03-06T19:08:28.230Z", + count: 1, + badge_id: 3, + user_id: 5, + granted_by_id: -1 + } + ], + top_categories: [] + } } }; From 536952f194924234459a874d30f16802eb30891b Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 17:13:20 -0500 Subject: [PATCH 33/67] adding back active nav pill --- app/assets/stylesheets/common/components/navs.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 12eb49b4ce0..75b797e261b 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -48,6 +48,7 @@ } } + &.active > a, > a.active { color: $secondary; background-color: $quaternary; From a917a9399054f27f1b6f10574466866365ebbb74 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 17:29:19 -0500 Subject: [PATCH 34/67] fix active class, remove style --- .../javascripts/discourse/components/edit-category-tab.js.es6 | 2 +- .../discourse/templates/components/edit-category-tab.hbs | 2 +- app/assets/stylesheets/common/components/navs.scss | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 index 87eed4459ca..7c2509f0680 100644 --- a/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-tab.js.es6 @@ -2,7 +2,7 @@ import { propertyEqual } from "discourse/lib/computed"; export default Ember.Component.extend({ tagName: "li", - classNameBindings: ["tabClassName"], + classNameBindings: ["active", "tabClassName"], tabClassName: function() { return "edit-category-" + this.get("tab"); diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs index b6108dfd6ad..4aeb37702a1 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs @@ -1 +1 @@ - {{title}} +{{title}} diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 75b797e261b..4718c1784e9 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -48,8 +48,7 @@ } } - &.active > a, - > a.active { + &.active > a{ color: $secondary; background-color: $quaternary; From 7d2cc330133accc753ee45df4bded927ae744b57 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 17:37:59 -0500 Subject: [PATCH 35/67] prettier --- app/assets/stylesheets/common/components/navs.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 4718c1784e9..1ce8f2767fd 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -48,7 +48,7 @@ } } - &.active > a{ + &.active > a { color: $secondary; background-color: $quaternary; From e233a69db096df568396f26ba792bd99646962cf Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 19:42:44 -0500 Subject: [PATCH 36/67] Customize nav pill fix --- app/assets/stylesheets/common/admin/customize.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index ccc8eed9561..8f4e0ae820f 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -410,7 +410,7 @@ li { margin-right: 0; display: flex; - + &.spacer { flex-grow: 1; } @@ -428,6 +428,11 @@ margin-bottom: 0; } + a.active { + background: $primary-medium; + color: $secondary; + } + a.blank:not(.active) { color: $primary-medium; } From 9d3c7d7b25dcddea1aa20280ca998609d0818bfe Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 19:52:34 -0500 Subject: [PATCH 37/67] prettier --- app/assets/stylesheets/common/admin/customize.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 8f4e0ae820f..fbd4e40edbf 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -410,7 +410,7 @@ li { margin-right: 0; display: flex; - + &.spacer { flex-grow: 1; } From be5e889b1b55de148b32ae864390a585e027b1ec Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 6 Mar 2019 20:27:40 -0500 Subject: [PATCH 38/67] UX: Poll style fix, button classes --- .../assets/javascripts/widgets/discourse-poll.js.es6 | 10 ++++++---- plugins/poll/assets/stylesheets/common/poll.scss | 4 ++++ plugins/poll/config/locales/client.en.yml | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 index 39d4d16049b..f3ae85f855b 100644 --- a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 +++ b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 @@ -420,7 +420,9 @@ createWidget("discourse-poll-buttons", { const castVotesDisabled = !attrs.canCastVotes; contents.push( this.attach("button", { - className: `btn cast-votes ${castVotesDisabled ? "" : "btn-primary"}`, + className: `btn cast-votes ${ + castVotesDisabled ? "btn-default" : "btn-primary" + }`, label: "poll.cast-votes.label", title: "poll.cast-votes.title", disabled: castVotesDisabled, @@ -433,7 +435,7 @@ createWidget("discourse-poll-buttons", { if (attrs.showResults || hideResultsDisabled) { contents.push( this.attach("button", { - className: "btn toggle-results", + className: "btn btn-default toggle-results", label: "poll.hide-results.label", title: "poll.hide-results.title", icon: "far-eye-slash", @@ -449,7 +451,7 @@ createWidget("discourse-poll-buttons", { } else { contents.push( this.attach("button", { - className: "btn toggle-results", + className: "btn btn-default toggle-results", label: "poll.show-results.label", title: "poll.show-results.title", icon: "far-eye", @@ -492,7 +494,7 @@ createWidget("discourse-poll-buttons", { if (!attrs.isAutomaticallyClosed) { contents.push( this.attach("button", { - className: "btn toggle-status", + className: "btn btn-default toggle-status", label: "poll.open.label", title: "poll.open.title", icon: "unlock-alt", diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss index 63f671d9eeb..c66bb0f1d8a 100644 --- a/plugins/poll/assets/stylesheets/common/poll.scss +++ b/plugins/poll/assets/stylesheets/common/poll.scss @@ -59,6 +59,10 @@ div.poll { margin: 0.25em 0; color: $primary-medium; } + .info-text + .info-text, + button + .info-text { + margin-left: 0.5em; + } } .poll-voters:not(:empty) { diff --git a/plugins/poll/config/locales/client.en.yml b/plugins/poll/config/locales/client.en.yml index a44a852cb6a..a57c3eb1aad 100644 --- a/plugins/poll/config/locales/client.en.yml +++ b/plugins/poll/config/locales/client.en.yml @@ -72,7 +72,7 @@ en: confirm: "Are you sure you want to close this poll?" automatic_close: - closes_in: "closes in %{timeLeft}" + closes_in: "Closes in %{timeLeft}." age: "closed %{age}" error_while_toggling_status: "Sorry, there was an error toggling the status of this poll." From caef0a56d1948b94e9775093420850170b17f824 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 7 Mar 2019 11:27:50 +0530 Subject: [PATCH 39/67] FEATURE: create a new message via URL without specifying user or group --- app/assets/javascripts/discourse/routes/new-message.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6 index abdf17e24bd..70691518829 100644 --- a/app/assets/javascripts/discourse/routes/new-message.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-message.js.es6 @@ -48,6 +48,8 @@ export default Discourse.Route.extend({ } }) .catch(() => bootbox.alert(I18n.t("generic_error"))); + } else { + e.send("createNewMessageViaParams", null, params.title, params.body); } }); } else { From 2c8e1d3578cd35112160f879052e347ac169dc86 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 7 Mar 2019 15:19:46 +0530 Subject: [PATCH 40/67] FEATURE: remove all expired invitations by default --- .../controllers/user-invited-show.js.es6 | 1 - app/controllers/invites_controller.rb | 2 +- app/models/invite.rb | 5 +++-- config/locales/client.en.yml | 6 +++--- spec/models/invite_spec.rb | 15 ++++++++++----- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index a157abee6ac..1bb292b72b9 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -90,7 +90,6 @@ export default Ember.Controller.extend({ Invite.rescindAll() .then(() => { this.set("rescindedAll", true); - this.get("model.invites").clear(); }) .catch(popupAjaxError); } diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 884f67ec8f7..30e06c2f65e 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -143,7 +143,7 @@ class InvitesController < ApplicationController def rescind_all_invites guardian.ensure_can_rescind_all_invites!(current_user) - Invite.rescind_all_invites_from(current_user) + Invite.rescind_all_expired_invites_from(current_user) render body: nil end diff --git a/app/models/invite.rb b/app/models/invite.rb index b2012405365..6e022424381 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -226,8 +226,9 @@ class Invite < ActiveRecord::Base end end - def self.rescind_all_invites_from(user) - Invite.where('invites.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ?', user.id).find_each do |invite| + def self.rescind_all_expired_invites_from(user) + Invite.where('invites.user_id IS NULL AND invites.email IS NOT NULL AND invited_by_id = ? AND invites.created_at < ?', + user.id, SiteSetting.invite_expiry_days.days.ago).find_each do |invite| invite.trash!(user) end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5ae7ddc8626..3ee598ca117 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1000,9 +1000,9 @@ en: expired: "This invite has expired." rescind: "Remove" rescinded: "Invite removed" - rescind_all: "Remove all Invites" - rescinded_all: "All Invites removed!" - rescind_all_confirm: "Are you sure you want to remove all invites?" + rescind_all: "Remove all Expired Invites" + rescinded_all: "All Expired Invites removed!" + rescind_all_confirm: "Are you sure you want to remove all expired invites?" reinvite: "Resend Invite" reinvite_all: "Resend all Invites" reinvite_all_confirm: "Are you sure you want to resend all invites?" diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 15ebea3f32d..bdf7a02faa4 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -477,16 +477,21 @@ describe Invite do end - describe '.rescind_all_invites_from' do - it 'removes all invites sent by a user' do + describe '.rescind_all_expired_invites_from' do + it 'removes all expired invites sent by a user' do + SiteSetting.invite_expiry_days = 1 user = Fabricate(:user) invite_1 = Fabricate(:invite, invited_by: user) invite_2 = Fabricate(:invite, invited_by: user) - Invite.rescind_all_invites_from(user) + expired_invite = Fabricate(:invite, invited_by: user) + expired_invite.update!(created_at: 2.days.ago) + Invite.rescind_all_expired_invites_from(user) invite_1.reload invite_2.reload - expect(invite_1.deleted_at).to be_present - expect(invite_2.deleted_at).to be_present + expired_invite.reload + expect(invite_1.deleted_at).to eq(nil) + expect(invite_2.deleted_at).to eq(nil) + expect(expired_invite.deleted_at).to be_present end end end From 6420b73c33819d5aa95a55b93cedaf0da2765715 Mon Sep 17 00:00:00 2001 From: Tim Lange Date: Thu, 7 Mar 2019 11:04:17 +0100 Subject: [PATCH 41/67] FIX: Changed poll age message to upcase (#7124) --- plugins/poll/config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/poll/config/locales/client.en.yml b/plugins/poll/config/locales/client.en.yml index a57c3eb1aad..3197397895f 100644 --- a/plugins/poll/config/locales/client.en.yml +++ b/plugins/poll/config/locales/client.en.yml @@ -73,7 +73,7 @@ en: automatic_close: closes_in: "Closes in %{timeLeft}." - age: "closed %{age}" + age: "Closed %{age}" error_while_toggling_status: "Sorry, there was an error toggling the status of this poll." error_while_casting_votes: "Sorry, there was an error casting your votes." From fc7938f7e081318ad52462bd2aa1c95e61fb5d03 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Mar 2019 11:31:04 +0000 Subject: [PATCH 42/67] REFACTOR: Migrate GoogleOAuth2Authenticator to use ManagedAuthenticator (#7120) https://meta.discourse.org/t/future-social-authentication-improvements/94691/3 --- app/models/google_user_info.rb | 27 ----- app/models/user.rb | 1 - app/services/user_anonymizer.rb | 1 - ...20190306154335_migrate_google_user_info.rb | 27 +++++ lib/auth/google_oauth2_authenticator.rb | 109 +----------------- lib/auth/managed_authenticator.rb | 14 ++- script/bulk_import/discourse_merger.rb | 7 +- .../auth/google_oauth2_authenticator_spec.rb | 16 ++- spec/jobs/invalidate_inactive_admins_spec.rb | 4 +- spec/models/user_spec.rb | 2 +- .../omniauth_callbacks_controller_spec.rb | 20 ++-- spec/services/user_anonymizer_spec.rb | 2 - spec/services/user_merger_spec.rb | 2 - 13 files changed, 71 insertions(+), 161 deletions(-) delete mode 100644 app/models/google_user_info.rb create mode 100644 db/migrate/20190306154335_migrate_google_user_info.rb diff --git a/app/models/google_user_info.rb b/app/models/google_user_info.rb deleted file mode 100644 index 343fe9945fd..00000000000 --- a/app/models/google_user_info.rb +++ /dev/null @@ -1,27 +0,0 @@ -class GoogleUserInfo < ActiveRecord::Base - belongs_to :user -end - -# == Schema Information -# -# Table name: google_user_infos -# -# id :integer not null, primary key -# user_id :integer not null -# google_user_id :string not null -# first_name :string -# last_name :string -# email :string -# gender :string -# name :string -# link :string -# profile_link :string -# picture :string -# created_at :datetime not null -# updated_at :datetime not null -# -# Indexes -# -# index_google_user_infos_on_google_user_id (google_user_id) UNIQUE -# index_google_user_infos_on_user_id (user_id) UNIQUE -# diff --git a/app/models/user.rb b/app/models/user.rb index 6c9e3be075d..97c8440a9a0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -66,7 +66,6 @@ class User < ActiveRecord::Base has_one :user_avatar, dependent: :destroy has_many :user_associated_accounts, dependent: :destroy has_one :github_user_info, dependent: :destroy - has_one :google_user_info, dependent: :destroy has_many :oauth2_user_infos, dependent: :destroy has_one :instagram_user_info, dependent: :destroy has_many :user_second_factors, dependent: :destroy diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb index ae480252b55..dfeb941d8db 100644 --- a/app/services/user_anonymizer.rb +++ b/app/services/user_anonymizer.rb @@ -53,7 +53,6 @@ class UserAnonymizer end @user.user_avatar.try(:destroy) - @user.google_user_info.try(:destroy) @user.github_user_info.try(:destroy) @user.single_sign_on_record.try(:destroy) @user.oauth2_user_infos.try(:destroy_all) diff --git a/db/migrate/20190306154335_migrate_google_user_info.rb b/db/migrate/20190306154335_migrate_google_user_info.rb new file mode 100644 index 00000000000..a3a9c0e1c37 --- /dev/null +++ b/db/migrate/20190306154335_migrate_google_user_info.rb @@ -0,0 +1,27 @@ +class MigrateGoogleUserInfo < ActiveRecord::Migration[5.2] + def up + execute <<~SQL + INSERT INTO user_associated_accounts ( + provider_name, + provider_uid, + user_id, + info, + last_used, + created_at, + updated_at + ) SELECT + 'google_oauth2', + google_user_id, + user_id, + json_build_object('email', email, 'first_name', first_name, 'last_name', last_name, 'name', name), + updated_at, + created_at, + updated_at + FROM google_user_infos + SQL + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index 297fa50976b..118d0812af5 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -1,5 +1,4 @@ -class Auth::GoogleOAuth2Authenticator < Auth::Authenticator - +class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator def name "google_oauth2" end @@ -8,77 +7,10 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator SiteSetting.enable_google_oauth2_logins end - def description_for_user(user) - info = GoogleUserInfo.find_by(user_id: user.id) - info&.email || info&.name || "" - end - - def can_revoke? - true - end - - def revoke(user, skip_remote: false) - info = GoogleUserInfo.find_by(user_id: user.id) - raise Discourse::NotFound if info.nil? - - # We get a temporary token from google upon login but do not need it, and do not store it. - # Therefore we do not have any way to revoke the token automatically on google's end - - info.destroy! - true - end - - def can_connect_existing_user? - true - end - - def after_authenticate(auth_hash, existing_account: nil) - session_info = parse_hash(auth_hash) - google_hash = session_info[:google] - - result = ::Auth::Result.new - result.email = session_info[:email] - result.email_valid = session_info[:email_valid] - result.name = session_info[:name] - - result.extra_data = google_hash - - user_info = ::GoogleUserInfo.find_by(google_user_id: google_hash[:google_user_id]) - - if existing_account && (user_info.nil? || existing_account.id != user_info.user_id) - user_info.destroy! if user_info - result.user = existing_account - user_info = GoogleUserInfo.create!({ user_id: result.user.id }.merge(google_hash)) - else - result.user = user_info&.user - end - - if !result.user && !result.email.blank? && result.email_valid - result.user = User.find_by_email(result.email) - if result.user - # we've matched an existing user to this login attempt... - if result.user.google_user_info && result.user.google_user_info.google_user_id != google_hash[:google_user_id] - # but the user has changed the google account used to log in... - if result.user.google_user_info.email != google_hash[:email] - # the user changed their email, go ahead and scrub the old record - result.user.google_user_info.destroy! - else - # same email address but different account? likely a takeover scenario - result.failed = true - result.failed_reason = I18n.t('errors.conflicting_google_user_id') - return result - end - end - ::GoogleUserInfo.create({ user_id: result.user.id }.merge(google_hash)) - end - end - - result - end - - def after_create_account(user, auth) - data = auth[:extra_data] - GoogleUserInfo.create({ user_id: user.id }.merge(data)) + def primary_email_verified?(auth_token) + # note, emails that come back from google via omniauth are always valid + # this protects against future regressions + auth_token[:extra][:raw_info][:email_verified] end def register_middleware(omniauth) @@ -95,37 +27,8 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") end - }, - skip_jwt: true + } } - # jwt encoding is causing auth to fail in quite a few conditions - # skipping omniauth.provider :google_oauth2, options end - - protected - - def parse_hash(hash) - extra = hash[:extra][:raw_info] - - h = {} - - h[:email] = hash[:info][:email] - h[:name] = hash[:info][:name] - h[:email_valid] = extra[:email_verified] - - h[:google] = { - google_user_id: hash[:uid] || extra[:sub], - email: extra[:email], - first_name: extra[:given_name], - last_name: extra[:family_name], - gender: extra[:gender], - name: extra[:name], - link: extra[:hd], - profile_link: extra[:profile], - picture: extra[:picture] - } - - h - end end diff --git a/lib/auth/managed_authenticator.rb b/lib/auth/managed_authenticator.rb index a56989c587d..1d21453a371 100644 --- a/lib/auth/managed_authenticator.rb +++ b/lib/auth/managed_authenticator.rb @@ -10,6 +10,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator true end + def primary_email_verified?(auth_token) + # Omniauth providers should only provide verified emails in the :info hash. + # This method allows additional checks to be added + true + end + def can_revoke? true end @@ -35,7 +41,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator end # Matching an account by email - if match_by_email && association.user.nil? && (user = User.find_by_email(auth_token.dig(:info, :email))) + if primary_email_verified?(auth_token) && + match_by_email && + association.user.nil? && + (user = User.find_by_email(auth_token.dig(:info, :email))) + UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end @@ -60,7 +70,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result.email = info[:email] result.name = "#{info[:first_name]} #{info[:last_name]}" result.username = info[:nickname] - result.email_valid = true if result.email + result.email_valid = primary_email_verified?(auth_token) if result.email result.extra_data = { provider: auth_token[:provider], uid: auth_token[:uid] diff --git a/script/bulk_import/discourse_merger.rb b/script/bulk_import/discourse_merger.rb index 2ad24990f29..cb85bc31e9b 100644 --- a/script/bulk_import/discourse_merger.rb +++ b/script/bulk_import/discourse_merger.rb @@ -151,7 +151,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base copy_model(c, skip_if_merged: true, is_a_user_model: true, skip_processing: true) end - [UserAssociatedAccount, GithubUserInfo, GoogleUserInfo, Oauth2UserInfo, + [UserAssociatedAccount, GithubUserInfo, Oauth2UserInfo, SingleSignOnRecord, EmailChangeRequest ].each do |c| copy_model(c, skip_if_merged: true, is_a_user_model: true) @@ -628,11 +628,6 @@ class BulkImport::DiscourseMerger < BulkImport::Base r end - def process_google_user_info(r) - return nil if GoogleUserInfo.where(google_user_id: r['google_user_id']).exists? - r - end - def process_oauth2_user_info(r) return nil if Oauth2UserInfo.where(uid: r['uid'], provider: r['provider']).exists? r diff --git a/spec/components/auth/google_oauth2_authenticator_spec.rb b/spec/components/auth/google_oauth2_authenticator_spec.rb index 17fb1082cec..49bc007201f 100644 --- a/spec/components/auth/google_oauth2_authenticator_spec.rb +++ b/spec/components/auth/google_oauth2_authenticator_spec.rb @@ -10,6 +10,7 @@ describe Auth::GoogleOAuth2Authenticator do user = Fabricate(:user) hash = { + provider: "google_oauth2", uid: "123456789", info: { name: "John Doe", @@ -35,6 +36,7 @@ describe Auth::GoogleOAuth2Authenticator do user = Fabricate(:user) hash = { + provider: "google_oauth2", uid: "123456789", info: { name: "John Doe", @@ -59,9 +61,10 @@ describe Auth::GoogleOAuth2Authenticator do user1 = Fabricate(:user) user2 = Fabricate(:user) - GoogleUserInfo.create!(user_id: user1.id, google_user_id: 100) + UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user1.id, provider_uid: 100) hash = { + provider: "google_oauth2", uid: "100", info: { name: "John Doe", @@ -79,14 +82,17 @@ describe Auth::GoogleOAuth2Authenticator do result = authenticator.after_authenticate(hash, existing_account: user2) expect(result.user.id).to eq(user2.id) - expect(GoogleUserInfo.exists?(user_id: user1.id)).to eq(false) - expect(GoogleUserInfo.exists?(user_id: user2.id)).to eq(true) + expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false) + expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true) end it 'can create a proper result for non existing users' do hash = { + provider: "google_oauth2", uid: "123456789", info: { + first_name: "Jane", + last_name: "Doe", name: "Jane Doe", email: "jane.doe@the.google.com" }, @@ -103,7 +109,7 @@ describe Auth::GoogleOAuth2Authenticator do result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) - expect(result.extra_data[:name]).to eq("Jane Doe") + expect(result.name).to eq("Jane Doe") end end @@ -116,7 +122,7 @@ describe Auth::GoogleOAuth2Authenticator do end it 'revokes correctly' do - GoogleUserInfo.create!(user_id: user.id, google_user_id: 12345, email: 'someuser@somedomain.tld') + UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user.id, provider_uid: 12345) expect(authenticator.can_revoke?).to eq(true) expect(authenticator.revoke(user)).to eq(true) expect(authenticator.description_for_user(user)).to eq("") diff --git a/spec/jobs/invalidate_inactive_admins_spec.rb b/spec/jobs/invalidate_inactive_admins_spec.rb index 6fd0f309c40..572120f9052 100644 --- a/spec/jobs/invalidate_inactive_admins_spec.rb +++ b/spec/jobs/invalidate_inactive_admins_spec.rb @@ -38,13 +38,13 @@ describe Jobs::InvalidateInactiveAdmins do before do GithubUserInfo.create!(user_id: not_seen_admin.id, screen_name: 'bob', github_user_id: 100) UserOpenId.create!(url: 'https://me.yahoo.com/id/123' , user_id: not_seen_admin.id, email: 'bob@example.com', active: true) - GoogleUserInfo.create!(user_id: not_seen_admin.id, google_user_id: 100, email: 'bob@example.com') + UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: not_seen_admin.id, provider_uid: 100, info: { email: "bob@google.account.com" }) end it 'removes the social logins' do subject expect(GithubUserInfo.where(user_id: not_seen_admin.id).exists?).to eq(false) - expect(GoogleUserInfo.where(user_id: not_seen_admin.id).exists?).to eq(false) + expect(UserAssociatedAccount.where(user_id: not_seen_admin.id).exists?).to eq(false) expect(UserOpenId.where(user_id: not_seen_admin.id).exists?).to eq(false) end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0bbea094a75..a1fbdcc0192 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -428,7 +428,7 @@ describe User do UserAssociatedAccount.create(user_id: user.id, provider_name: "twitter", provider_uid: "1", info: { nickname: "sam" }) UserAssociatedAccount.create(user_id: user.id, provider_name: "facebook", provider_uid: "1234", info: { email: "test@example.com" }) UserAssociatedAccount.create(user_id: user.id, provider_name: "instagram", provider_uid: "examplel123123", info: { nickname: "sam" }) - GoogleUserInfo.create(user_id: user.id, email: "sam@sam.com", google_user_id: 1) + UserAssociatedAccount.create(user_id: user.id, provider_name: "google_oauth2", provider_uid: "1", info: { email: "sam@sam.com" }) GithubUserInfo.create(user_id: user.id, screen_name: "sam", github_user_id: 1) user.reload diff --git a/spec/requests/omniauth_callbacks_controller_spec.rb b/spec/requests/omniauth_callbacks_controller_spec.rb index d4b424344e7..8296ac3362e 100644 --- a/spec/requests/omniauth_callbacks_controller_spec.rb +++ b/spec/requests/omniauth_callbacks_controller_spec.rb @@ -96,7 +96,9 @@ RSpec.describe Users::OmniauthCallbacksController do uid: '123545', info: OmniAuth::AuthHash::InfoHash.new( email: email, - name: 'Some name' + name: 'Some name', + first_name: "Some", + last_name: "name" ), extra: { raw_info: OmniAuth::AuthHash.new( @@ -107,7 +109,7 @@ RSpec.describe Users::OmniauthCallbacksController do gender: 'male', name: "Some name Huh", ) - }, + } ) Rails.application.env_config["omniauth.auth"] = OmniAuth.config.mock_auth[:google_oauth2] @@ -262,7 +264,7 @@ RSpec.describe Users::OmniauthCallbacksController do @sso.return_sso_url = "http://somewhere.over.rainbow/sso" cookies[:sso_payload] = @sso.payload - GoogleUserInfo.create!(google_user_id: '12345', user: user) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user) OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( provider: 'google_oauth2', @@ -299,7 +301,7 @@ RSpec.describe Users::OmniauthCallbacksController do context 'when user has not verified his email' do before do - GoogleUserInfo.create!(google_user_id: '12345', user: user) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user) user.update!(active: false) OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( @@ -341,8 +343,8 @@ RSpec.describe Users::OmniauthCallbacksController do context 'when attempting reconnect' do let(:user2) { Fabricate(:user) } before do - GoogleUserInfo.create!(google_user_id: '12345', user: user) - GoogleUserInfo.create!(google_user_id: '123456', user: user2) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '12345', user: user) + UserAssociatedAccount.create!(provider_name: "google_oauth2", provider_uid: '123456', user: user2) OmniAuth.config.mock_auth[:google_oauth2] = OmniAuth::AuthHash.new( provider: 'google_oauth2', @@ -385,7 +387,7 @@ RSpec.describe Users::OmniauthCallbacksController do get "/auth/google_oauth2/callback.json" expect(response.status).to eq(200) expect(session[:current_user_id]).to eq(user2.id) - expect(GoogleUserInfo.count).to eq(2) + expect(UserAssociatedAccount.count).to eq(2) end it 'should reconnect if parameter supplied' do @@ -402,7 +404,7 @@ RSpec.describe Users::OmniauthCallbacksController do expect(session[:auth_reconnect]).to eq(nil) # Disconnect - GoogleUserInfo.find_by(user_id: user.id).destroy + UserAssociatedAccount.find_by(user_id: user.id).destroy # Reconnect flow: get "/auth/google_oauth2?reconnect=true" @@ -414,7 +416,7 @@ RSpec.describe Users::OmniauthCallbacksController do expect(response.status).to eq(200) expect(JSON.parse(response.body)["authenticated"]).to eq(true) expect(session[:current_user_id]).to eq(user.id) - expect(GoogleUserInfo.count).to eq(1) + expect(UserAssociatedAccount.count).to eq(1) end end diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb index f1b396032e9..87a652925e7 100644 --- a/spec/services/user_anonymizer_spec.rb +++ b/spec/services/user_anonymizer_spec.rb @@ -190,7 +190,6 @@ describe UserAnonymizer do end it "removes external auth assocations" do - user.google_user_info = GoogleUserInfo.create(user_id: user.id, google_user_id: "google@gmail.com") user.github_user_info = GithubUserInfo.create(user_id: user.id, screen_name: "example", github_user_id: "examplel123123") user.user_associated_accounts = [UserAssociatedAccount.create(user_id: user.id, provider_uid: "example", provider_name: "facebook")] user.single_sign_on_record = SingleSignOnRecord.create(user_id: user.id, external_id: "example", last_payload: "looks good") @@ -198,7 +197,6 @@ describe UserAnonymizer do UserOpenId.create(user_id: user.id, email: user.email, url: "http://example.com/openid", active: true) make_anonymous user.reload - expect(user.google_user_info).to eq(nil) expect(user.github_user_info).to eq(nil) expect(user.user_associated_accounts).to be_empty expect(user.single_sign_on_record).to eq(nil) diff --git a/spec/services/user_merger_spec.rb b/spec/services/user_merger_spec.rb index 304bb0cdb64..86025a5ef4e 100644 --- a/spec/services/user_merger_spec.rb +++ b/spec/services/user_merger_spec.rb @@ -978,7 +978,6 @@ describe UserMerger do it "deletes external auth infos of source user" do UserAssociatedAccount.create(user_id: source_user.id, provider_name: "facebook", provider_uid: "1234") GithubUserInfo.create(user_id: source_user.id, screen_name: "example", github_user_id: "examplel123123") - GoogleUserInfo.create(user_id: source_user.id, google_user_id: "google@gmail.com") Oauth2UserInfo.create(user_id: source_user.id, uid: "example", provider: "example") SingleSignOnRecord.create(user_id: source_user.id, external_id: "example", last_payload: "looks good") UserOpenId.create(user_id: source_user.id, email: source_user.email, url: "http://example.com/openid", active: true) @@ -987,7 +986,6 @@ describe UserMerger do expect(UserAssociatedAccount.where(user_id: source_user.id).count).to eq(0) expect(GithubUserInfo.where(user_id: source_user.id).count).to eq(0) - expect(GoogleUserInfo.where(user_id: source_user.id).count).to eq(0) expect(Oauth2UserInfo.where(user_id: source_user.id).count).to eq(0) expect(SingleSignOnRecord.where(user_id: source_user.id).count).to eq(0) expect(UserOpenId.where(user_id: source_user.id).count).to eq(0) From a9648e8fd11299554cdbba5d4b5aec0aef95464a Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 7 Mar 2019 17:55:47 +0530 Subject: [PATCH 43/67] onebox version bump - FIX: respect code indentation --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 836fbf583b5..45af6c03c35 100644 --- a/Gemfile +++ b/Gemfile @@ -44,7 +44,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.81' +gem 'onebox', '1.8.82' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 2fda9211997..299c7f91ac8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -261,7 +261,7 @@ GEM omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.81) + onebox (1.8.82) htmlentities (~> 4.3) moneta (~> 1.0) multi_json (~> 1.11) @@ -515,7 +515,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.81) + onebox (= 1.8.82) openid-redis-store pg pry-nav From a2fdd4089c51588fdf93bc7863801e4ad9c73172 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Mar 2019 13:40:12 +0100 Subject: [PATCH 44/67] FIX: nav-pills not highlighted on user preferences page (#7126) --- app/assets/stylesheets/common/components/navs.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 1ce8f2767fd..4f73ad7966a 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -48,7 +48,8 @@ } } - &.active > a { + &.active > a, + a.active { color: $secondary; background-color: $quaternary; From ad87b0d6624ce7b9bc97d7d7f2db361789ad3779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 7 Mar 2019 14:15:30 +0100 Subject: [PATCH 45/67] Make "uploads:recover_from_tombstone" call the "uploads:recover" rake task --- lib/tasks/uploads.rake | 100 ++--------------------------------------- 1 file changed, 4 insertions(+), 96 deletions(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 3bc5879669a..1ca1d3e2be3 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -495,102 +495,6 @@ def list_missing_uploads(skip_optimized: false) Discourse.store.list_missing_uploads(skip_optimized: skip_optimized) end -################################################################################ -# Recover from tombstone # -################################################################################ - -task "uploads:recover_from_tombstone" => :environment do - if ENV["RAILS_DB"] - recover_from_tombstone - else - RailsMultisite::ConnectionManagement.each_connection { recover_from_tombstone } - end -end - -def recover_from_tombstone - if Discourse.store.external? - puts "This task only works for internal storages." - return - end - - begin - previous_image_size = SiteSetting.max_image_size_kb - previous_attachment_size = SiteSetting.max_attachment_size_kb - previous_extensions = SiteSetting.authorized_extensions - - SiteSetting.max_image_size_kb = 10 * 1024 - SiteSetting.max_attachment_size_kb = 10 * 1024 - SiteSetting.authorized_extensions = "*" - - current_db = RailsMultisite::ConnectionManagement.current_db - public_path = Rails.root.join("public") - paths = Dir.glob(File.join(public_path, 'uploads', 'tombstone', current_db, '**', '*.*')) - max = paths.size - - paths.each_with_index do |path, index| - filename = File.basename(path) - printf("%9d / %d (%5.1f%%)\n", (index + 1), max, (((index + 1).to_f / max.to_f) * 100).round(1)) - - Post.where("raw LIKE ?", "%#{filename}%").find_each do |post| - doc = Nokogiri::HTML::fragment(post.raw) - updated = false - - image_urls = doc.css("img[src]").map { |img| img["src"] } - attachment_urls = doc.css("a.attachment[href]").map { |a| a["href"] } - - (image_urls + attachment_urls).each do |url| - next if !url.start_with?("/uploads/") - next if Upload.exists?(url: url) - - puts "Restoring #{path}..." - tombstone_path = File.join(public_path, 'uploads', 'tombstone', url.gsub(/^\/uploads\//, "")) - - if File.exists?(tombstone_path) - File.open(tombstone_path) do |file| - new_upload = UploadCreator.new(file, File.basename(url)).create_for(Discourse::SYSTEM_USER_ID) - - if new_upload.persisted? - puts "Restored into #{new_upload.url}" - DbHelper.remap(url, new_upload.url) - updated = true - else - puts "Failed to create upload for #{url}: #{new_upload.errors.full_messages}." - end - end - else - puts "Failed to find file (#{tombstone_path}) in tombstone." - end - end - - post.rebake! if updated - end - - sha1 = File.basename(filename, File.extname(filename)) - short_url = "upload://#{Base62.encode(sha1.hex)}" - - Post.where("raw LIKE ?", "%#{short_url}%").find_each do |post| - puts "Restoring #{path}..." - - File.open(path) do |file| - new_upload = UploadCreator.new(file, filename).create_for(Discourse::SYSTEM_USER_ID) - - if new_upload.persisted? - puts "Restored into #{new_upload.short_url}" - DbHelper.remap(short_url, new_upload.short_url) if short_url != new_upload.short_url - post.rebake! - else - puts "Failed to create upload for #{filename}: #{new_upload.errors.full_messages}." - end - end - end - end - ensure - SiteSetting.max_image_size_kb = previous_image_size - SiteSetting.max_attachment_size_kb = previous_attachment_size - SiteSetting.authorized_extensions = previous_extensions - end -end - ################################################################################ # regenerate_missing_optimized # ################################################################################ @@ -795,6 +699,10 @@ task "uploads:fix_incorrect_extensions" => :environment do UploadFixer.fix_all_extensions end +task "uploads:recover_from_tombstone" => :environment do + Rake::Task["uploads:recover"].invoke +end + task "uploads:recover" => :environment do require_dependency "upload_recovery" From bc52437153adbded95e5a61dc63ee8bd2bf3cfda Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Mar 2019 14:03:05 +0000 Subject: [PATCH 46/67] FIX: Add additional rescue for assets:precompile This error can be caused if assets are precompiled before the database has been migrated --- lib/tasks/assets.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 0e2c6525dfe..2815f4440c5 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -58,7 +58,7 @@ task 'assets:precompile:css' => 'environment' do STDERR.puts "Compiling css for #{db} #{Time.zone.now}" begin Stylesheet::Manager.precompile_css - rescue PG::UndefinedColumn => e + rescue PG::UndefinedColumn, ActiveModel::MissingAttributeError => e STDERR.puts "#{e.class} #{e.message}: #{e.backtrace.join("\n")}" STDERR.puts "Skipping precompilation of CSS cause schema is old, you are precompiling prior to running migrations." end From da64b90d4f19307522289253b6540e525909f303 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 7 Mar 2019 12:51:12 -0500 Subject: [PATCH 47/67] FIX: missing translations when "en" set as fallbackLocale When a plugin registers a language and sets fallbackLocale="en", fallback strings were missing. This commit strips any duplicate ":en" symbols when loading merged translations. --- lib/js_locale_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 21fba9804ef..377d11e5508 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -86,7 +86,7 @@ module JsLocaleHelper end def self.load_translations_merged(*locales) - locales = locales.compact + locales = locales.uniq.compact @loaded_merges ||= {} @loaded_merges[locales.join('-')] ||= begin all_translations = {} From 837452794b52b927b9bd27d58fa021aa337795d0 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 7 Mar 2019 13:21:42 -0500 Subject: [PATCH 48/67] Target active nav pill with single style --- .../javascripts/discourse/components/navigation-item.js.es6 | 3 ++- .../discourse/templates/components/edit-category-tab.hbs | 2 +- app/assets/stylesheets/common/components/navs.scss | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6 index 80a9fdc3014..d0a39b0f9bf 100644 --- a/app/assets/javascripts/discourse/components/navigation-item.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6 @@ -47,7 +47,8 @@ export default Ember.Component.extend( this.set("hidden", false); } - buffer.push(``); + buffer.push(``); + if (content.get("hasIcon")) { buffer.push(""); } diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs index 4aeb37702a1..7be1e910609 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-tab.hbs @@ -1 +1 @@ -{{title}} +{{title}} diff --git a/app/assets/stylesheets/common/components/navs.scss b/app/assets/stylesheets/common/components/navs.scss index 4f73ad7966a..622493ee0d8 100644 --- a/app/assets/stylesheets/common/components/navs.scss +++ b/app/assets/stylesheets/common/components/navs.scss @@ -48,7 +48,6 @@ } } - &.active > a, a.active { color: $secondary; background-color: $quaternary; From ccff1d2dbb8912dd0d0a990b30ad47f61af2d4fd Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 7 Mar 2019 13:30:39 -0500 Subject: [PATCH 49/67] prettier --- .../javascripts/discourse/components/navigation-item.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6 index d0a39b0f9bf..ae47bab740e 100644 --- a/app/assets/javascripts/discourse/components/navigation-item.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6 @@ -47,7 +47,9 @@ export default Ember.Component.extend( this.set("hidden", false); } - buffer.push(``); + buffer.push( + `` + ); if (content.get("hasIcon")) { buffer.push(""); From 78f8114989f9fbe4d4fc491f26b62da0ca6b5229 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 7 Mar 2019 21:48:26 +0100 Subject: [PATCH 50/67] FEATURE: Allow discourse script to skip disabling of emails after restore --- lib/backup_restore/restorer.rb | 9 ++++++--- script/discourse | 7 ++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index f382df5ea36..57870cfab95 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -29,6 +29,7 @@ module BackupRestore @client_id = opts[:client_id] @filename = opts[:filename] @publish_to_message_bus = opts[:publish_to_message_bus] || false + @disable_emails = opts.fetch(:disable_emails, true) ensure_restore_is_enabled ensure_no_operation_is_running @@ -402,9 +403,11 @@ module BackupRestore log "Reloading site settings..." SiteSetting.refresh! - log "Disabling outgoing emails for non-staff users..." - user = User.find_by_email(@user_info[:email]) || Discourse.system_user - SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + if @disable_emails + log "Disabling outgoing emails for non-staff users..." + user = User.find_by_email(@user_info[:email]) || Discourse.system_user + SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + end end def clear_emoji_cache diff --git a/script/discourse b/script/discourse index 205b9be545b..3b830db810c 100755 --- a/script/discourse +++ b/script/discourse @@ -106,6 +106,7 @@ class DiscourseCLI < Thor end desc "restore", "Restore a Discourse backup" + option :disable_emails, type: :boolean, default: true def restore(filename = nil) if File.exist?('/usr/local/bin/discourse') @@ -132,7 +133,11 @@ class DiscourseCLI < Thor begin puts "Starting restore: #{filename}" - restorer = BackupRestore::Restorer.new(Discourse.system_user.id, filename: filename) + restorer = BackupRestore::Restorer.new( + Discourse.system_user.id, + filename: filename, + disable_emails: options[:disable_emails] + ) restorer.run puts 'Restore done.' rescue BackupRestore::FilenameMissingError From f6ef4f8c3fceb004558bbf63fa18d539dfde4dba Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 7 Mar 2019 22:42:20 +0100 Subject: [PATCH 51/67] FEATURE: Add missing S3 regions --- app/models/s3_region_site_setting.rb | 3 +++ config/locales/client.en.yml | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/s3_region_site_setting.rb b/app/models/s3_region_site_setting.rb index d931a14b203..9f195d4a908 100644 --- a/app/models/s3_region_site_setting.rb +++ b/app/models/s3_region_site_setting.rb @@ -17,15 +17,18 @@ class S3RegionSiteSetting < EnumSiteSetting 'ap-south-1', 'ap-southeast-1', 'ap-southeast-2', + 'ca-central-1', 'cn-north-1', 'cn-northwest-1', 'eu-central-1', + 'eu-north-1', 'eu-west-1', 'eu-west-2', 'eu-west-3', 'sa-east-1', 'us-east-1', 'us-east-2', + 'us-gov-east-1', 'us-gov-west-1', 'us-west-1', 'us-west-2', diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3ee598ca117..6141724c631 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -192,15 +192,18 @@ en: ap_south_1: "Asia Pacific (Mumbai)" ap_southeast_1: "Asia Pacific (Singapore)" ap_southeast_2: "Asia Pacific (Sydney)" + ca_central_1: "Canada (Central)" cn_north_1: "China (Beijing)" cn_northwest_1: "China (Ningxia)" eu_central_1: "EU (Frankfurt)" + eu_north_1: "EU (Stockholm)" eu_west_1: "EU (Ireland)" eu_west_2: "EU (London)" eu_west_3: "EU (Paris)" - sa_east_1: "South America (Sao Paulo)" + sa_east_1: "South America (São Paulo)" us_east_1: "US East (N. Virginia)" us_east_2: "US East (Ohio)" + us_gov_east_1: "AWS GovCloud (US-East)" us_gov_west_1: "AWS GovCloud (US)" us_west_1: "US West (N. California)" us_west_2: "US West (Oregon)" From 6e26358380d0eb4d54cb28cb3d63c48f0d383b2b Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 7 Mar 2019 21:09:57 -0500 Subject: [PATCH 52/67] REFACTOR: streamline like button styles, clarify for themeing, enable tab focus --- .../stylesheets/desktop/topic-post.scss | 118 ++++++++---------- 1 file changed, 55 insertions(+), 63 deletions(-) diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 4a59ef97e00..484efd885e5 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -47,60 +47,69 @@ section.post-menu-area { nav.post-controls { padding: 0; .like-button { + // Like button wrapper display: inline-flex; - .like-count { - color: dark-light-choose($primary-low-mid, $secondary-high); - } - .widget-button { - background: none; - } + color: $primary-low-mid; + margin-right: 0.15em; &:hover { - background: $primary-low; - .like-count { + // Like button wrapper on hover + button { + background: $primary-low; color: $primary-medium; + } + } + button { + margin-left: 0; + margin-right: 0; + &.my-likes { + // Like count on my posts + .d-icon { + color: $primary-low-mid; + padding-left: 0.45em; + } + } + &.like { + // Like button with 0 likes + &.d-hover { + background: $love-low; + .d-icon { + color: $love; + } + } + } + &.has-like { + // Like button after I've liked + .d-icon { + color: $love; + } + &.d-hover { + background: $primary-low; + .d-icon { + color: $primary-medium; + } + } + } + &[disabled] { + // Disabled like button + cursor: not-allowed; + } + &.like-count { + // Like count button + &:not(.my-likes) { + padding-right: 0; + } &.d-hover { color: $primary; } - } - .d-hover { - background: none; - } - .d-icon { - color: $love; + + .toggle-like { + // Like button when like count is present + padding-left: 0.45em; + &.d-hover { + background: $primary-low; + } + } } } - &:active { - box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.4); - .widget-button { - box-shadow: none; - } - } - .like { - &:focus { - background: none; - } - } - .like-count { - font-size: $font-up-1; - margin-left: 0; - .d-icon { - padding-left: 10px; - color: dark-light-choose($primary-low-mid, $secondary-high); - } - &.my-likes { - margin-right: -2px; - } - &.regular-likes { - margin-right: -12px; - } - } - .toggle-like { - padding: 8px 8px; - margin-left: 2px; - } - } - .highlight-action { - color: dark-light-choose($primary-medium, $secondary-high); } a, button { @@ -186,23 +195,6 @@ nav.post-controls { color: $secondary; } } - &.like.d-hover, - &.like:focus { - color: $love; - background: $love-low; - .d-icon { - color: $love; - } - } - &.has-like .d-icon { - color: $love; - } - &.has-like[disabled]:hover { - background: transparent; - } - &.has-like[disabled]:active { - box-shadow: none; - } &.bookmark { padding: 8px 11px; &.bookmarked .d-icon { From cd94ac9453c46fc4e46f529b5c370ad4caa4d68a Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 7 Mar 2019 22:32:27 -0500 Subject: [PATCH 53/67] FIX: remove padding from onebox site icon in composer preview --- app/assets/stylesheets/common/d-editor.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index b29aeb16f2e..86e16bbb2f5 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -206,7 +206,8 @@ .d-editor-preview img { padding-bottom: 1.4em; &.emoji, - &.avatar { + &.avatar, + &.site-icon { padding-bottom: 0; } } From c90267db52dfc329d3f4e56f34e4ca2c7a74f829 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 8 Mar 2019 09:23:26 +0100 Subject: [PATCH 54/67] =?UTF-8?q?FIX:=20do=20not=20display=20add/remove=20?= =?UTF-8?q?participants=20if=20you=20can=E2=80=99t=20actually=20do=20it=20?= =?UTF-8?q?(#7125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../widgets/private-message-map.js.es6 | 21 ++++++++++++------- .../stylesheets/common/base/topic-post.scss | 5 ++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 index 40b5ce38c02..1ce3119a75f 100644 --- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 @@ -152,14 +152,17 @@ export default createWidget("private-message-map", { } const result = [h(`div.participants${hideNamesClass}`, participants)]; + const controls = []; - const controls = [ - this.attach("button", { - action: "toggleEditing", - label: "private_message_info.edit", - className: "btn btn-default add-remove-participant-btn" - }) - ]; + if (attrs.canRemoveAllowedUsers || attrs.canRemoveSelfId) { + controls.push( + this.attach("button", { + action: "toggleEditing", + label: "private_message_info.edit", + className: "btn btn-default add-remove-participant-btn" + }) + ); + } if (attrs.canInvite && this.state.isEditing) { controls.push( @@ -171,7 +174,9 @@ export default createWidget("private-message-map", { ); } - result.push(h("div.controls", controls)); + if (controls.length) { + result.push(h("div.controls", controls)); + } return result; }, diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 91463df6301..09247ad12b4 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -357,7 +357,10 @@ aside.quote { display: flex; flex-wrap: wrap; align-items: center; - margin-bottom: 0.5em; + + & + .controls { + margin-top: 0.5em; + } &.hide-names .user { .username, From 3acf8a95f3a759c5c05b4c86cf9e9b9d3f00c057 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Fri, 8 Mar 2019 09:23:44 +0100 Subject: [PATCH 55/67] UX: various tweaks to search-menu (#7114) --- .../javascripts/discourse/lib/search.js.es6 | 27 +++- .../widgets/search-menu-results.js.es6 | 138 +++++++++++------- .../stylesheets/common/base/search-menu.scss | 113 +++++++++----- lib/search.rb | 2 +- 4 files changed, 184 insertions(+), 96 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/search.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6 index 32c226228d4..28380ddc93d 100644 --- a/app/assets/javascripts/discourse/lib/search.js.es6 +++ b/app/assets/javascripts/discourse/lib/search.js.es6 @@ -46,11 +46,26 @@ export function translateResults(results, opts) { results.groups = results.groups .map(group => { - const groupName = Handlebars.Utils.escapeExpression(group.name); + const name = Handlebars.Utils.escapeExpression(group.name); + const fullName = Handlebars.Utils.escapeExpression( + group.full_name || group.display_name + ); + const flairUrl = Ember.isEmpty(group.flair_url) + ? null + : Handlebars.Utils.escapeExpression(group.flair_url); + const flairColor = Handlebars.Utils.escapeExpression(group.flair_color); + const flairBgColor = Handlebars.Utils.escapeExpression( + group.flair_bg_color + ); + return { id: group.id, - name: groupName, - url: Discourse.getURL(`/g/${groupName}`) + flairUrl, + flairColor, + flairBgColor, + fullName, + name, + url: Discourse.getURL(`/g/${name}`) }; }) .compact(); @@ -72,10 +87,10 @@ export function translateResults(results, opts) { if (groupedSearchResult) { [ ["topic", "posts"], - ["category", "categories"], - ["tag", "tags"], ["user", "users"], - ["group", "groups"] + ["group", "groups"], + ["category", "categories"], + ["tag", "tags"] ].forEach(function(pair) { const type = pair[0]; const name = pair[1]; diff --git a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 index 69ecf7ace58..b6acdf85873 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-results.js.es6 @@ -90,16 +90,48 @@ createSearchResult({ } }); +createSearchResult({ + type: "group", + linkField: "url", + builder(group) { + const fullName = escapeExpression(group.fullName); + const name = escapeExpression(group.name); + const groupNames = [h("span.name", fullName || name)]; + + if (fullName) { + groupNames.push(h("span.slug", name)); + } + + let avatarFlair; + if (group.flairUrl) { + avatarFlair = this.attach("avatar-flair", { + primary_group_flair_url: group.flairUrl, + primary_group_flair_bg_color: group.flairBgColor, + primary_group_flair_color: group.flairColor, + primary_group_name: name + }); + } else { + avatarFlair = iconNode("users"); + } + + const groupResultContents = [avatarFlair, h("div.group-names", groupNames)]; + + return h("div.group-result", groupResultContents); + } +}); + createSearchResult({ type: "user", linkField: "path", builder(u) { - const userTitles = [h("span.username", formatUsername(u.username))]; + const userTitles = []; if (u.name) { userTitles.push(h("span.name", u.name)); } + userTitles.push(h("span.username", formatUsername(u.username))); + const userResultContents = [ avatarImg("small", { template: u.avatar_template, @@ -112,21 +144,6 @@ createSearchResult({ } }); -createSearchResult({ - type: "group", - linkField: "url", - builder(group) { - const groupName = escapeExpression(group.name); - return h( - "span", - { - className: `group-${groupName} discourse-group` - }, - [iconNode("users"), h("span", groupName)] - ); - } -}); - createSearchResult({ type: "topic", linkField: "url", @@ -174,19 +191,12 @@ createWidget("search-menu-results", { const resultTypes = results.resultTypes || []; const mainResultsContent = []; - const classificationContents = []; - const otherContents = []; - const assignContainer = (type, node) => { - if (["topic"].includes(type)) { - mainResultsContent.push(node); - } else if (["category", "tag"].includes(type)) { - classificationContents.push(node); - } else { - otherContents.push(node); - } - }; + const usersAndGroups = []; + const categoriesAndTags = []; + const usersAndGroupsMore = []; + const categoriesAndTagsMore = []; - resultTypes.forEach(rt => { + const buildMoreNode = result => { const more = []; const moreArgs = { @@ -194,23 +204,45 @@ createWidget("search-menu-results", { contents: () => [I18n.t("more"), "..."] }; - if (rt.moreUrl) { + if (result.moreUrl) { more.push( - this.attach("link", $.extend(moreArgs, { href: rt.moreUrl })) + this.attach("link", $.extend(moreArgs, { href: result.moreUrl })) ); - } else if (rt.more) { + } else if (result.more) { more.push( this.attach( "link", $.extend(moreArgs, { action: "moreOfType", - actionParam: rt.type, + actionParam: result.type, className: "filter filter-type" }) ) ); } + if (more.length) { + return more; + } + }; + + const assignContainer = (result, node) => { + if (["topic"].includes(result.type)) { + mainResultsContent.push(node); + } + + if (["user", "group"].includes(result.type)) { + usersAndGroups.push(node); + usersAndGroupsMore.push(buildMoreNode(result)); + } + + if (["category", "tag"].includes(result.type)) { + categoriesAndTags.push(node); + categoriesAndTagsMore.push(buildMoreNode(result)); + } + }; + + resultTypes.forEach(rt => { const resultNodeContents = [ this.attach(rt.componentName, { searchContextEnabled: attrs.searchContextEnabled, @@ -220,14 +252,14 @@ createWidget("search-menu-results", { }) ]; - if (more.length) { - resultNodeContents.push(h("div.show-more", more)); + if (["topic"].includes(rt.type)) { + const more = buildMoreNode(rt); + if (more) { + resultNodeContents.push(h("div.show-more", more)); + } } - assignContainer( - rt.type, - h(`div.${rt.componentName}`, resultNodeContents) - ); + assignContainer(rt, h(`div.${rt.componentName}`, resultNodeContents)); }); const content = []; @@ -236,27 +268,25 @@ createWidget("search-menu-results", { content.push(h("div.main-results", mainResultsContent)); } - if (classificationContents.length || otherContents.length) { - const secondaryResultsContent = []; + if (usersAndGroups.length || categoriesAndTags.length) { + const secondaryResultsContents = []; - if (classificationContents.length) { - secondaryResultsContent.push( - h("div.classification-results", classificationContents) - ); + secondaryResultsContents.push(usersAndGroups); + secondaryResultsContents.push(usersAndGroupsMore); + + if (usersAndGroups.length && categoriesAndTags.length) { + secondaryResultsContents.push(h("div.separator")); } - if (otherContents.length) { - secondaryResultsContent.push(h("div.other-results", otherContents)); - } + secondaryResultsContents.push(categoriesAndTags); + secondaryResultsContents.push(categoriesAndTagsMore); - content.push( - h( - `div.secondary-results${ - mainResultsContent.length ? "" : ".no-main-results" - }`, - secondaryResultsContent - ) + const secondaryResults = h( + "div.secondary-results", + secondaryResultsContents ); + + content.push(secondaryResults); } return content; diff --git a/app/assets/stylesheets/common/base/search-menu.scss b/app/assets/stylesheets/common/base/search-menu.scss index a38e5b168ec..6da0cdf298c 100644 --- a/app/assets/stylesheets/common/base/search-menu.scss +++ b/app/assets/stylesheets/common/base/search-menu.scss @@ -76,26 +76,22 @@ flex-direction: column; flex: 1 1 auto; - .classification-results { - border-bottom: 1px solid $primary-low; + .separator { margin-bottom: 1em; - padding-bottom: 1em; - } - - .search-result-category { + margin-top: 1em; + height: 1px; + background: $primary-low; } .search-result-tag { - .list { - .item { - display: inline-flex; + .discourse-tag { + font-size: $font-down-1; + } + } - .widget-link.search-link { - display: inline; - font-size: $font-0; - padding: 5px; - } - } + .search-result-category { + .widget-link { + margin-bottom: 0; } } @@ -108,12 +104,71 @@ } } - .discourse-group { - display: inline-block; - word-break: break-all; + .group-result { + display: flex; + align-items: center; - .d-icon { - margin-right: s(1); + .d-icon, + .avatar-flair { + min-width: 25px; + margin-right: 0.5em; + + .d-icon { + margin-right: 0; + } + } + + .avatar-flair-image { + background-repeat: no-repeat; + background-size: 100% 100%; + min-height: 25px; + } + + .group-names { + display: flex; + flex-direction: column; + overflow: auto; + line-height: $line-height-medium; + + &:hover { + .name, + .slug { + color: $primary-high; + } + } + + .name, + .slug { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .name { + font-weight: 700; + } + + .slug { + font-size: $font-down-1; + color: $primary-high; + } + } + } + } + + .search-result-category, + .search-result-user, + .search-result-group, + .search-result-tag { + .list { + display: block; + + .item { + .widget-link.search-link { + flex: 1; + font-size: $font-0; + padding: 5px; + } } } } @@ -145,29 +200,17 @@ .username { color: dark-light-choose($primary-high, $secondary-low); - font-size: $font-0; - font-weight: 700; + font-size: $font-down-1; } .name { color: dark-light-choose($primary-high, $secondary-low); - font-size: $font-down-1; + font-size: $font-0; + font-weight: 700; } } } } - - &.no-main-results .search-result-user { - .user-titles { - flex-direction: row; - align-items: center; - - .name { - margin: 0 0 0 0.25em; - font-size: $font-0; - } - } - } } .show-more { diff --git a/lib/search.rb b/lib/search.rb index 56ebc31a431..3ffbda872fb 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -687,7 +687,7 @@ class Search def groups_search groups = Group .visible_groups(@guardian.user, "name ASC", include_everyone: false) - .where("groups.name ILIKE ?", "%#{@term}%") + .where("name ILIKE :term OR full_name ILIKE :term", term: "%#{@term}%") groups.each { |group| @results.add(group) } end From 65464969cd3d25421884ab17ac5650f6527a7145 Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Fri, 8 Mar 2019 00:24:14 -0800 Subject: [PATCH 56/67] Change searchTearm to searchTerm (#7130) --- .../javascripts/discourse/controllers/user-invited-show.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index 1bb292b72b9..1c4a6c460c8 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -24,7 +24,7 @@ export default Ember.Controller.extend({ this.set("searchTerm", ""); }, - @observes("searchTearm") + @observes("searchTerm") _searchTermChanged: debounce(function() { Invite.findInvitedBy( this.get("user"), From 35942f7c7c9510161c42018543ac609254dafdbd Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Fri, 8 Mar 2019 10:48:35 +0200 Subject: [PATCH 57/67] FEATURE: Special call-out for new / returning posters. (#7115) --- .../discourse/lib/transform-post.js.es6 | 7 +++++ .../javascripts/discourse/widgets/post.js.es6 | 28 +++++++++++++++++++ .../stylesheets/common/base/topic-post.scss | 19 +++++++++++++ app/models/post.rb | 6 ++++ app/serializers/post_serializer.rb | 18 ++++++++++++ config/locales/client.en.yml | 4 +++ config/locales/server.en.yml | 2 ++ config/site_settings.yml | 2 ++ lib/post_creator.rb | 16 +++++++++++ lib/svg_sprite/svg_sprite.rb | 1 + lib/topic_view.rb | 2 +- spec/components/post_creator_spec.rb | 28 +++++++++++++++++++ spec/models/post_spec.rb | 23 +++++++++++++++ test/javascripts/widgets/post-test.js.es6 | 19 +++++++++++++ 14 files changed, 174 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index b63c2249147..e0d08665ef4 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -134,6 +134,13 @@ export default function transformPost( postAtts.topicUrl = topic.get("url"); postAtts.isSaving = post.isSaving; + if (post.post_notice_type) { + postAtts.postNoticeType = post.post_notice_type; + if (postAtts.postNoticeType === "returning") { + postAtts.postNoticeTime = new Date(post.post_notice_time); + } + } + const showPMMap = topic.archetype === "private_message" && post.post_number === 1; if (showPMMap) { diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index fbbda456aa5..956dafd3583 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -13,6 +13,7 @@ import { formatUsername } from "discourse/lib/utilities"; import hbs from "discourse/widgets/hbs-compiler"; +import { relativeAge } from "discourse/lib/formatter"; function transformWithCallbacks(post) { let transformed = transformBasicPost(post); @@ -427,6 +428,29 @@ createWidget("post-contents", { } }); +createWidget("post-notice", { + tagName: "div.post-notice", + + html(attrs) { + let text, icon; + if (attrs.postNoticeType === "first") { + icon = "hands-helping"; + text = I18n.t("post.notice.first", { user: attrs.username }); + } else if (attrs.postNoticeType === "returning") { + icon = "far-smile"; + text = I18n.t("post.notice.return", { + user: attrs.username, + time: relativeAge(attrs.postNoticeTime, { + format: "tiny", + addAgo: true + }) + }); + } + + return h("p", [iconNode(icon), text]); + } +}); + createWidget("post-body", { tagName: "div.topic-body.clearfix", @@ -505,6 +529,10 @@ createWidget("post-article", { ); } + if (attrs.postNoticeType) { + rows.push(h("div.row", [this.attach("post-notice", attrs)])); + } + rows.push( h("div.row", [ this.attach("post-avatar", attrs), diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 09247ad12b4..8f5f630cce6 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -864,3 +864,22 @@ a.mention-group { margin-bottom: 1em; } } + +.post-notice { + background-color: $tertiary-low; + border-top: 1px solid $primary-low; + color: $primary; + padding: 1em; + width: calc( + #{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} + + 3px + ); + + p { + margin: 0; + } + + .d-icon { + margin-right: 1em; + } +} diff --git a/app/models/post.rb b/app/models/post.rb index 00a4db52fa0..73f52ecb6eb 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -194,6 +194,7 @@ class Post < ActiveRecord::Base def recover! super update_flagged_posts_count + delete_post_notices recover_public_post_actions TopicLink.extract_from(self) QuotedPost.extract_from(self) @@ -381,6 +382,11 @@ class Post < ActiveRecord::Base PostAction.update_flagged_posts_count end + def delete_post_notices + self.custom_fields.delete("post_notice_type") + self.custom_fields.delete("post_notice_time") + end + def recover_public_post_actions PostAction.publics .with_deleted diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 42686fae37a..916e2cdb6ea 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -70,6 +70,8 @@ class PostSerializer < BasicPostSerializer :is_auto_generated, :action_code, :action_code_who, + :post_notice_type, + :post_notice_time, :last_wiki_edit, :locked, :excerpt @@ -363,6 +365,22 @@ class PostSerializer < BasicPostSerializer include_action_code? && action_code_who.present? end + def post_notice_type + post_custom_fields["post_notice_type"] + end + + def include_post_notice_type? + post_notice_type.present? + end + + def post_notice_time + post_custom_fields["post_notice_time"] + end + + def include_post_notice_time? + post_notice_time.present? + end + def locked true end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 6141724c631..bb1eb3917a7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2149,6 +2149,10 @@ en: one: "view 1 hidden reply" other: "view {{count}} hidden replies" + notice: + first: "This is the first time {{user}} has posted — let's welcome them to our community!" + return: "It's been a while since we've seen {{user}} — their last post was in {{time}}." + unread: "Post is unread" has_replies: one: "{{count}} Reply" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index f457a08e08a..300697c5952 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1901,6 +1901,8 @@ en: max_allowed_message_recipients: "Maximum recipients allowed in a message." watched_words_regular_expressions: "Watched words are regular expressions." + returning_users_days: "How many days should pass before a user is considered to be returning." + default_email_digest_frequency: "How often users receive summary emails by default." default_include_tl0_in_digests: "Include posts from new users in summary emails by default. Users can change this in their preferences." default_email_personal_messages: "Send an email when someone messages the user by default." diff --git a/config/site_settings.yml b/config/site_settings.yml index 627f4d5b07d..8bd22447f95 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -807,6 +807,8 @@ posting: default: false client: true shadowed_by_global: true + returning_users_days: + default: 60 email: email_time_window_mins: diff --git a/lib/post_creator.rb b/lib/post_creator.rb index d8bee4047ca..6f773199bbf 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -165,6 +165,7 @@ class PostCreator transaction do build_post_stats create_topic + create_post_notice save_post extract_links track_topic @@ -508,6 +509,21 @@ class PostCreator @user.update_attributes(last_posted_at: @post.created_at) end + def create_post_notice + last_post_time = Post.where(user_id: @user.id) + .order(created_at: :desc) + .limit(1) + .pluck(:created_at) + .first + + if !last_post_time + @post.custom_fields["post_notice_type"] = "first" + elsif SiteSetting.returning_users_days > 0 && last_post_time < SiteSetting.returning_users_days.days.ago + @post.custom_fields["post_notice_type"] = "returning" + @post.custom_fields["post_notice_time"] = last_post_time + end + end + def publish return if @opts[:import_mode] || @post.post_number == 1 @post.publish_change_to_clients! :created diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index 7037c7a0e54..f6946b2e325 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -118,6 +118,7 @@ module SvgSprite "globe", "globe-americas", "hand-point-right", + "hands-helping", "heading", "heart", "home", diff --git a/lib/topic_view.rb b/lib/topic_view.rb index c40f82ff0b5..4f8c0ac3668 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -18,7 +18,7 @@ class TopicView end def self.default_post_custom_fields - @default_post_custom_fields ||= ["action_code_who"] + @default_post_custom_fields ||= ["action_code_who", "post_notice_type", "post_notice_time"] end def self.post_custom_fields_whitelisters diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 8774c6402e9..97a4960f319 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -1238,4 +1238,32 @@ describe PostCreator do end end end + + context "#create_post_notice" do + let(:user) { Fabricate(:user) } + let(:new_user) { Fabricate(:user) } + let(:returning_user) { Fabricate(:user) } + + it "generates post notices" do + # new users + post = PostCreator.create(new_user, title: "one of my first topics", raw: "one of my first posts") + expect(post.custom_fields["post_notice_type"]).to eq("first") + post = PostCreator.create(new_user, title: "another one of my first topics", raw: "another one of my first posts") + expect(post.custom_fields["post_notice_type"]).to eq(nil) + + # returning users + SiteSetting.returning_users_days = 30 + old_post = Fabricate(:post, user: returning_user, created_at: 31.days.ago) + post = PostCreator.create(returning_user, title: "this is a returning topic", raw: "this is a post") + expect(post.custom_fields["post_notice_type"]).to eq("returning") + expect(post.custom_fields["post_notice_time"]).to eq(old_post.created_at.to_s) + end + + it "does not generate post notices" do + Fabricate(:post, user: user, created_at: 3.days.ago) + post = PostCreator.create(user, title: "this is another topic", raw: "this is my another post") + expect(post.custom_fields["post_notice_type"]).to eq(nil) + expect(post.custom_fields["post_notice_time"]).to eq(nil) + end + end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 9ddfccfb3fd..de5c367d19b 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -134,6 +134,29 @@ describe Post do end end + context 'a post with notices' do + let(:post) { + post = Fabricate(:post, post_args) + post.custom_fields["post_notice_type"] = "returning" + post.custom_fields["post_notice_time"] = 1.day.ago + post + } + + before do + post.trash! + post.reload + end + + describe 'recovery' do + it 'deletes notices' do + post.recover! + + expect(post.custom_fields).not_to have_key("post_notice_type") + expect(post.custom_fields).not_to have_key("post_notice_time") + end + end + end + end describe 'flagging helpers' do diff --git a/test/javascripts/widgets/post-test.js.es6 b/test/javascripts/widgets/post-test.js.es6 index 12b5726ff14..5dc1b8ad50d 100644 --- a/test/javascripts/widgets/post-test.js.es6 +++ b/test/javascripts/widgets/post-test.js.es6 @@ -852,3 +852,22 @@ widgetTest("pm map", { assert.equal(find(".private-message-map .user").length, 1); } }); + +widgetTest("post notice", { + template: '{{mount-widget widget="post" args=args}}', + beforeEach() { + this.set("args", { + postNoticeType: "returning", + postNoticeTime: new Date("2010-01-01 12:00:00 UTC"), + username: "codinghorror" + }); + }, + test(assert) { + assert.equal( + find(".post-notice") + .text() + .trim(), + I18n.t("post.notice.return", { user: "codinghorror", time: "Jan '10" }) + ); + } +}); From 2312caccdc27f7947254a1a31baa61d3d5c94eb0 Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Fri, 8 Mar 2019 10:49:34 +0200 Subject: [PATCH 58/67] FEATURE: Skip small actions when counting replies in PMs. (#7108) --- app/models/topic.rb | 62 ++++++++++++++++++++++++---- lib/post_creator.rb | 6 ++- spec/components/post_creator_spec.rb | 22 ++++++++++ 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/app/models/topic.rb b/app/models/topic.rb index 419a06357ee..028b6fc7a55 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -527,10 +527,10 @@ class Topic < ActiveRecord::Base end # Atomically creates the next post number - def self.next_post_number(topic_id, reply = false, whisper = false) + def self.next_post_number(topic_id, opts = {}) highest = DB.query_single("SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", topic_id).first.to_i - if whisper + if opts[:whisper] result = DB.query_single(<<~SQL, highest, topic_id) UPDATE topics @@ -543,13 +543,15 @@ class Topic < ActiveRecord::Base else - reply_sql = reply ? ", reply_count = reply_count + 1" : "" + reply_sql = opts[:reply] ? ", reply_count = reply_count + 1" : "" + posts_sql = opts[:post] ? ", posts_count = posts_count + 1" : "" result = DB.query_single(<<~SQL, highest: highest, topic_id: topic_id) UPDATE topics SET highest_staff_post_number = :highest + 1, - highest_post_number = :highest + 1#{reply_sql}, - posts_count = posts_count + 1 + highest_post_number = :highest + 1 + #{reply_sql} + #{posts_sql} WHERE id = :topic_id RETURNING highest_post_number SQL @@ -585,6 +587,43 @@ class Topic < ActiveRecord::Base posts_count = Y.posts_count FROM X, Y WHERE + topics.archetype <> 'private_message' AND + X.topic_id = topics.id AND + Y.topic_id = topics.id AND ( + topics.highest_staff_post_number <> X.highest_post_number OR + topics.highest_post_number <> Y.highest_post_number OR + topics.last_posted_at <> Y.last_posted_at OR + topics.posts_count <> Y.posts_count + ) + SQL + + DB.exec <<~SQL + WITH + X as ( + SELECT topic_id, + COALESCE(MAX(post_number), 0) highest_post_number + FROM posts + WHERE deleted_at IS NULL + GROUP BY topic_id + ), + Y as ( + SELECT topic_id, + coalesce(MAX(post_number), 0) highest_post_number, + count(*) posts_count, + max(created_at) last_posted_at + FROM posts + WHERE deleted_at IS NULL AND post_type <> 3 AND post_type <> 4 + GROUP BY topic_id + ) + UPDATE topics + SET + highest_staff_post_number = X.highest_post_number, + highest_post_number = Y.highest_post_number, + last_posted_at = Y.last_posted_at, + posts_count = Y.posts_count + FROM X, Y + WHERE + topics.archetype = 'private_message' AND X.topic_id = topics.id AND Y.topic_id = topics.id AND ( topics.highest_staff_post_number <> X.highest_post_number OR @@ -597,32 +636,39 @@ class Topic < ActiveRecord::Base # If a post is deleted we have to update our highest post counters def self.reset_highest(topic_id) + archetype = Topic.where(id: topic_id).pluck(:archetype).first + + # ignore small_action replies for private messages + post_type = archetype == Archetype.private_message ? " AND post_type <> #{Post.types[:small_action]}" : '' + result = DB.query_single(<<~SQL, topic_id: topic_id) UPDATE topics SET - highest_staff_post_number = ( + highest_staff_post_number = ( SELECT COALESCE(MAX(post_number), 0) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL ), - highest_post_number = ( + highest_post_number = ( SELECT COALESCE(MAX(post_number), 0) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 + #{post_type} ), posts_count = ( SELECT count(*) FROM posts WHERE deleted_at IS NULL AND topic_id = :topic_id AND post_type <> 4 + #{post_type} ), - last_posted_at = ( SELECT MAX(created_at) FROM posts WHERE topic_id = :topic_id AND deleted_at IS NULL AND post_type <> 4 + #{post_type} ) WHERE id = :topic_id RETURNING highest_post_number diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 6f773199bbf..a74f43f030d 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -248,7 +248,11 @@ class PostCreator post.word_count = post.raw.scan(/[[:word:]]+/).size whisper = post.post_type == Post.types[:whisper] - post.post_number ||= Topic.next_post_number(post.topic_id, post.reply_to_post_number.present?, whisper) + increase_posts_count = !post.topic&.private_message? || post.post_type != Post.types[:small_action] + post.post_number ||= Topic.next_post_number(post.topic_id, + reply: post.reply_to_post_number.present?, + whisper: whisper, + post: increase_posts_count) cooking_options = post.cooking_options || {} cooking_options[:topic_id] = post.topic_id diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 97a4960f319..2fe93452b72 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -776,6 +776,28 @@ describe PostCreator do expect(post.topic.topic_allowed_users.where(user_id: admin2.id).count).to eq(0) end + + it 'does not increase posts count for small actions' do + topic = Fabricate(:private_message_topic, user: Fabricate(:user)) + + Fabricate(:post, topic: topic) + + 1.upto(3) do |i| + user = Fabricate(:user) + topic.invite(topic.user, user.username) + topic.reload + expect(topic.posts_count).to eq(1) + expect(topic.posts.where(post_type: Post.types[:small_action]).count).to eq(i) + end + + Fabricate(:post, topic: topic) + Topic.reset_highest(topic.id) + expect(topic.reload.posts_count).to eq(2) + + Fabricate(:post, topic: topic) + Topic.reset_all_highest! + expect(topic.reload.posts_count).to eq(3) + end end context "warnings" do From b101065bad2c2e451c531ff59b0d7884f6a1e7d6 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 8 Mar 2019 08:58:06 +0000 Subject: [PATCH 59/67] FIX: Sanitize theme settings properly before injecting into stylesheets (#7031) --- lib/stylesheet/importer.rb | 5 ++--- spec/models/theme_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb index 49ff26d8e70..19d9676120f 100644 --- a/lib/stylesheet/importer.rb +++ b/lib/stylesheet/importer.rb @@ -144,9 +144,8 @@ COMMENT end def to_scss_variable(name, value) - escaped = value.to_s.gsub('"', "\\22") - escaped.gsub!("\n", "\\A") - "$#{name}: unquote(\"#{escaped}\");\n" + escaped = SassC::Script::Value::String.quote(value, sass: true) + "$#{name}: unquote(#{escaped});\n" end def imports(asset, parent_path) diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 0b4a332c9a7..3b0b442e697 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -310,6 +310,18 @@ HTML scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id) expect(scss).to include("font-size:30px") + + # Escapes correctly. If not, compiling this would throw an exception + setting.value = <<~MULTILINE + \#{$fakeinterpolatedvariable} + andanothervalue 'withquotes'; margin: 0; + MULTILINE + + theme.set_field(target: :common, name: :scss, value: 'body {font-size: quote($font-size)}') + theme.save! + + scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: theme.id) + expect(scss).to include('font-size:"#{$fakeinterpolatedvariable}\a andanothervalue \'withquotes\'; margin: 0;\a"') end it "allows values to be used in JS" do From 9db05a895ab737bf379575ca23e4b12ffb9e964e Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 8 Mar 2019 09:16:13 +0000 Subject: [PATCH 60/67] DEV: Add job_id to sidkiq log This makes it easier to correlate 'pending' logs from the same job --- app/jobs/base.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index aec6fd86de9..643bbb89394 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -17,13 +17,14 @@ module Jobs class Base class JobInstrumenter - def initialize(job_class:, opts:, db:) + def initialize(job_class:, opts:, db:, jid:) return unless enabled? @data = {} @data["hostname"] = `hostname`.strip # Hostname @data["pid"] = Process.pid # Pid @data["database"] = db # DB name - multisite db name it ran on + @data["job_id"] = jid # Job unique ID @data["job_name"] = job_class.name # Job Name - eg: Jobs::AboutStats @data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular @data["opts"] = opts.to_json # Params - json encoded params for the job @@ -185,7 +186,7 @@ module Jobs exception = {} RailsMultisite::ConnectionManagement.with_connection(db) do - job_instrumenter = JobInstrumenter.new(job_class: self.class, opts: opts, db: db) + job_instrumenter = JobInstrumenter.new(job_class: self.class, opts: opts, db: db, jid: jid) begin I18n.locale = SiteSetting.default_locale || "en" I18n.ensure_all_loaded! From 2434e48d4d68978ed7a60392765447df5165e0e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 8 Mar 2019 10:50:33 +0100 Subject: [PATCH 61/67] UX: Fix post notice on mobile --- app/assets/stylesheets/common/base/topic-post.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 8f5f630cce6..3c0a1633949 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -870,10 +870,7 @@ a.mention-group { border-top: 1px solid $primary-low; color: $primary; padding: 1em; - width: calc( - #{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} + - 3px - ); + margin-bottom: 1em; p { margin: 0; From 7ff994b6eae555f6f9a220cdc655beeb101b8129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 8 Mar 2019 10:52:47 +0100 Subject: [PATCH 62/67] UX: post-notice max width on desktop --- app/assets/stylesheets/common/base/topic-post.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 3c0a1633949..1a9a4cf73bd 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -871,6 +871,9 @@ a.mention-group { color: $primary; padding: 1em; margin-bottom: 1em; + max-width: calc( + #{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} + 3px + ); p { margin: 0; From ad12b2a23d50be3bbd7748145d3d487f25241a1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 8 Mar 2019 11:22:20 +0100 Subject: [PATCH 63/67] FIX: only call 'unlink' for tempfiles --- app/models/optimized_image.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index fbcc5621d0f..74a45ad97b6 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -407,8 +407,8 @@ class OptimizedImage < ActiveRecord::Base # just ditch the optimized image if there was any errors optimized_image.destroy ensure - file&.unlink file&.close + file&.unlink if file&.respond_to?(:unlink) end end end From ac8425ad3007a796d788e725d68a39d31fcc5733 Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Fri, 8 Mar 2019 12:23:44 +0200 Subject: [PATCH 64/67] FIX: Date format and styling for post notices Follow-up to 35942f7. --- app/assets/stylesheets/common/base/topic-post.scss | 4 ++-- app/assets/stylesheets/mobile/topic-post.scss | 4 ++++ lib/post_creator.rb | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 1a9a4cf73bd..246b4e28d8c 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -870,9 +870,9 @@ a.mention-group { border-top: 1px solid $primary-low; color: $primary; padding: 1em; - margin-bottom: 1em; max-width: calc( - #{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} + 3px + #{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} + + 3px ); p { diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 69e58dbeee0..831e47b0ddc 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -475,3 +475,7 @@ span.highlighted { margin-bottom: 0; } } + +.post-notice { + margin-bottom: 1em; +} diff --git a/lib/post_creator.rb b/lib/post_creator.rb index a74f43f030d..2dd11e649db 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -524,7 +524,7 @@ class PostCreator @post.custom_fields["post_notice_type"] = "first" elsif SiteSetting.returning_users_days > 0 && last_post_time < SiteSetting.returning_users_days.days.ago @post.custom_fields["post_notice_type"] = "returning" - @post.custom_fields["post_notice_time"] = last_post_time + @post.custom_fields["post_notice_time"] = last_post_time.iso8601 end end From e2510d79ccc21cae2ddfa612bebf2dfcdb120feb Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 8 Mar 2019 10:31:49 +0000 Subject: [PATCH 65/67] DEV: Improve thread-safety of sidekiq logging --- app/jobs/base.rb | 76 +++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 643bbb89394..f210308de46 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -19,50 +19,53 @@ module Jobs class JobInstrumenter def initialize(job_class:, opts:, db:, jid:) return unless enabled? - @data = {} + self.class.mutex.synchronize do + @data = {} - @data["hostname"] = `hostname`.strip # Hostname - @data["pid"] = Process.pid # Pid - @data["database"] = db # DB name - multisite db name it ran on - @data["job_id"] = jid # Job unique ID - @data["job_name"] = job_class.name # Job Name - eg: Jobs::AboutStats - @data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular - @data["opts"] = opts.to_json # Params - json encoded params for the job + @data["hostname"] = `hostname`.strip # Hostname + @data["pid"] = Process.pid # Pid + @data["database"] = db # DB name - multisite db name it ran on + @data["job_id"] = jid # Job unique ID + @data["job_name"] = job_class.name # Job Name - eg: Jobs::AboutStats + @data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular + @data["opts"] = opts.to_json # Params - json encoded params for the job - @data["status"] = 'pending' - @start_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @data["status"] = 'pending' + @start_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC) - self.class.ensure_interval_logging! - @@active_jobs ||= [] - @@active_jobs << self + self.class.ensure_interval_logging! + @@active_jobs ||= [] + @@active_jobs << self - MethodProfiler.ensure_discourse_instrumentation! - MethodProfiler.start + MethodProfiler.ensure_discourse_instrumentation! + MethodProfiler.start + end end def stop(exception:) return unless enabled? + self.class.mutex.synchronize do + profile = MethodProfiler.stop - profile = MethodProfiler.stop + @@active_jobs.delete(self) - @@active_jobs.delete(self) + @data["duration"] = profile[:total_duration] # Duration - length in seconds it took to run + @data["sql_duration"] = profile.dig(:sql, :duration) || 0 # Sql Duration (s) + @data["sql_calls"] = profile.dig(:sql, :calls) || 0 # Sql Statements - how many statements ran + @data["redis_duration"] = profile.dig(:redis, :duration) || 0 # Redis Duration (s) + @data["redis_calls"] = profile.dig(:redis, :calls) || 0 # Redis commands + @data["net_duration"] = profile.dig(:net, :duration) || 0 # Redis Duration (s) + @data["net_calls"] = profile.dig(:net, :calls) || 0 # Redis commands - @data["duration"] = profile[:total_duration] # Duration - length in seconds it took to run - @data["sql_duration"] = profile.dig(:sql, :duration) || 0 # Sql Duration (s) - @data["sql_calls"] = profile.dig(:sql, :calls) || 0 # Sql Statements - how many statements ran - @data["redis_duration"] = profile.dig(:redis, :duration) || 0 # Redis Duration (s) - @data["redis_calls"] = profile.dig(:redis, :calls) || 0 # Redis commands - @data["net_duration"] = profile.dig(:net, :duration) || 0 # Redis Duration (s) - @data["net_calls"] = profile.dig(:net, :calls) || 0 # Redis commands + if exception.present? + @data["exception"] = exception # Exception - if job fails a json encoded exception + @data["status"] = 'failed' + else + @data["status"] = 'success' # Status - fail, success, pending + end - if exception.present? - @data["exception"] = exception # Exception - if job fails a json encoded exception - @data["status"] = 'failed' - else - @data["status"] = 'success' # Status - fail, success, pending + write_to_log end - - write_to_log end def self.raw_log(message) @@ -97,14 +100,21 @@ module Jobs ENV["DISCOURSE_LOG_SIDEKIQ"] == "1" end + def self.mutex + @@mutex ||= Mutex.new + end + def self.ensure_interval_logging! interval = ENV["DISCOURSE_LOG_SIDEKIQ_INTERVAL"] return if !interval + interval = interval.to_i @@interval_thread ||= Thread.new do begin loop do - sleep interval.to_i - @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval.to_i } + sleep interval + mutex.synchronize do + @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval } + end end rescue Exception => e Discourse.warn_exception(e, message: "Sidekiq interval logging thread terminated unexpectedly") From 347470114675c8b60c96358853f1863565afed08 Mon Sep 17 00:00:00 2001 From: Dan Ungureanu Date: Fri, 8 Mar 2019 12:44:54 +0200 Subject: [PATCH 66/67] DEV: Fix build. --- spec/components/post_creator_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 2fe93452b72..86ebf54ba1c 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -1278,7 +1278,7 @@ describe PostCreator do old_post = Fabricate(:post, user: returning_user, created_at: 31.days.ago) post = PostCreator.create(returning_user, title: "this is a returning topic", raw: "this is a post") expect(post.custom_fields["post_notice_type"]).to eq("returning") - expect(post.custom_fields["post_notice_time"]).to eq(old_post.created_at.to_s) + expect(post.custom_fields["post_notice_time"]).to eq(old_post.created_at.iso8601) end it "does not generate post notices" do From 0a4562253ed0fd09c23de304aecfb6f8e08da391 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Fri, 8 Mar 2019 10:56:36 +0000 Subject: [PATCH 67/67] DEV: Add 'starting' event to sidekiq log when interval logging enabled --- app/jobs/base.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index f210308de46..746f325c094 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -30,7 +30,12 @@ module Jobs @data["job_type"] = job_class.try(:scheduled?) ? "scheduled" : "regular" # Job Type - either s for scheduled or r for regular @data["opts"] = opts.to_json # Params - json encoded params for the job - @data["status"] = 'pending' + if ENV["DISCOURSE_LOG_SIDEKIQ_INTERVAL"] + @data["status"] = "starting" + write_to_log + end + + @data["status"] = "pending" @start_timestamp = Process.clock_gettime(Process::CLOCK_MONOTONIC) self.class.ensure_interval_logging!