From 3811b8aa4c9fc08bf84d5da55b7927f4e300b9b5 Mon Sep 17 00:00:00 2001 From: Robin Ward <robin.ward@gmail.com> Date: Tue, 16 Feb 2016 12:25:01 -0500 Subject: [PATCH 1/6] `withPluginAPI` shim to updated plugins will not raise errors --- app/assets/javascripts/discourse/lib/plugin-api.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index b7a7ece91..f5d66d738 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -15,3 +15,8 @@ export function decorateCooked(container, cb) { decorate(container.lookupFactory('view:embedded-post'), 'didInsertElement', cb); decorate(container.lookupFactory('view:user-stream'), 'didInsertElement', cb); } + +// Will be backported so plugins in the new format will not raise errors +export function withPluginApi(version) { + console.warn(`Plugin API v${version} is not supported`); +} From 63b9d1c64588bb2d7a3fe3c84741edf0cd836a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Tue, 16 Feb 2016 18:29:23 +0100 Subject: [PATCH 2/6] FIX: sends an email notifcation when a user's post is linked --- app/controllers/admin/email_templates_controller.rb | 3 ++- app/mailers/user_notifications.rb | 7 +++++++ app/models/user_email_observer.rb | 4 ++++ config/locales/server.en.yml | 12 ++++++++++++ spec/models/user_email_observer_spec.rb | 8 ++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index 9d872c998..ccbb3eaf6 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -26,7 +26,8 @@ class Admin::EmailTemplatesController < Admin::AdminController "user_notifications.user_invited_to_private_message_pm", "user_notifications.user_invited_to_topic", "user_notifications.user_mentioned", "user_notifications.user_posted", "user_notifications.user_posted_pm", - "user_notifications.user_quoted", "user_notifications.user_replied"] + "user_notifications.user_quoted", "user_notifications.user_replied", + "user_notifications.user_linked"] end def show diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 517294278..2f4423cb1 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -110,6 +110,13 @@ class UserNotifications < ActionMailer::Base notification_email(user, opts) end + def user_linked(user, opts) + opts[:allow_reply_by_email] = true + opts[:use_site_subject] = true + opts[:show_category_in_subject] = true + notification_email(user, opts) + end + def user_mentioned(user, opts) opts[:allow_reply_by_email] = true opts[:use_site_subject] = true diff --git a/app/models/user_email_observer.rb b/app/models/user_email_observer.rb index db9dbdc73..31128235e 100644 --- a/app/models/user_email_observer.rb +++ b/app/models/user_email_observer.rb @@ -28,6 +28,10 @@ class UserEmailObserver < ActiveRecord::Observer enqueue :user_replied end + def linked + enqueue :user_linked + end + def private_message enqueue_private(:user_private_message) end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2e7ef4768..281d53654 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2095,6 +2095,18 @@ en: --- %{respond_instructions} + user_linked: + subject_template: "[%{site_name}] %{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + --- + %{respond_instructions} + user_mentioned: subject_template: "[%{site_name}] %{topic_title}" text_body_template: | diff --git a/spec/models/user_email_observer_spec.rb b/spec/models/user_email_observer_spec.rb index 0f1150c4d..c0b8da164 100644 --- a/spec/models/user_email_observer_spec.rb +++ b/spec/models/user_email_observer_spec.rb @@ -99,6 +99,14 @@ describe UserEmailObserver do include_examples "enqueue_public" end + context 'user_linked' do + let(:type) { :user_linked } + let(:delay) { SiteSetting.email_time_window_mins.minutes } + let!(:notification) { create_notification(11) } + + include_examples "enqueue_public" + end + context 'user_posted' do let(:type) { :user_posted } let(:delay) { SiteSetting.email_time_window_mins.minutes } From 63cda2262307d43160de0a84d538a646cd288723 Mon Sep 17 00:00:00 2001 From: Robin Ward <robin.ward@gmail.com> Date: Tue, 16 Feb 2016 16:43:59 -0500 Subject: [PATCH 3/6] Upgrade `withPluginApi` to support non-api callbacks --- .../discourse/lib/plugin-api.js.es6 | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index f5d66d738..5a700db5f 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -16,7 +16,29 @@ export function decorateCooked(container, cb) { decorate(container.lookupFactory('view:user-stream'), 'didInsertElement', cb); } -// Will be backported so plugins in the new format will not raise errors -export function withPluginApi(version) { +// This is backported so plugins in the new format will not raise errors +// +// To upgrade your plugin for backwards compatibility, you can add code in this +// form: +// +// function newApiCode(api) { +// // api.xyz(); +// } +// +// function oldCode() { +// // your pre-PluginAPI code goes here. You will be able to delete this +// // code once the `PluginAPI` has been rolled out to all versions of +// // Discourse you want to support. +// } +// +// // `newApiCode` will use API version 0.1, if no API support then +// // `oldCode` will be called +// withPluginApi('0.1', newApiCode, { noApi: oldCode }); +// +export function withPluginApi(version, apiCodeCallback, opts) { console.warn(`Plugin API v${version} is not supported`); + + if (opts && opts.noApi) { + return opts.noApi(); + } } From 3829c78526d2a552ff61767f81694650151cac6b Mon Sep 17 00:00:00 2001 From: Sam <sam.saffron@gmail.com> Date: Wed, 17 Feb 2016 15:46:19 +1100 Subject: [PATCH 4/6] PERF: shift most user options out of the user table As it stands we load up user records quite frequently on the topic pages, this in turn pulls all the columns for the users being selected, just to discard them after they are loaded New structure keeps all options in a discrete table, this is better organised and allows us to easily add more column without worrying about bloating the user table --- .../javascripts/discourse/models/user.js.es6 | 32 ++++---- .../discourse/templates/user/preferences.hbs | 26 +++---- app/controllers/email_controller.rb | 9 ++- .../notify_mailing_list_subscribers.rb | 3 +- app/jobs/regular/user_email.rb | 6 +- app/jobs/scheduled/enqueue_digest_emails.rb | 4 +- app/mailers/user_notifications.rb | 2 +- app/models/user.rb | 62 ++------------- app/models/user_email_observer.rb | 4 +- app/models/user_option.rb | 29 +++++++ app/serializers/current_user_serializer.rb | 23 +++++- app/serializers/user_option_serializer.rb | 20 +++++ app/serializers/user_serializer.rb | 23 ++---- app/services/anonymous_shadow_creator.rb | 7 +- app/services/user_anonymizer.rb | 13 ++-- app/services/user_updater.rb | 24 ++++-- db/fixtures/009_users.rb | 8 +- db/migrate/20160225050317_add_user_options.rb | 76 +++++++++++++++++++ lib/guardian/post_guardian.rb | 2 +- spec/components/guardian_spec.rb | 4 +- spec/controllers/email_controller_spec.rb | 29 ++++--- spec/jobs/user_email_spec.rb | 7 +- spec/models/user_email_observer_spec.rb | 4 +- spec/models/user_spec.rb | 76 +++++++++++-------- spec/serializers/user_serializer_spec.rb | 21 ++++- .../services/anonymous_shadow_creator_spec.rb | 3 + spec/services/user_anonymizer_spec.rb | 14 ++-- spec/services/user_updater_spec.rb | 23 ++++-- 28 files changed, 363 insertions(+), 191 deletions(-) create mode 100644 app/models/user_option.rb create mode 100644 app/serializers/user_option_serializer.rb create mode 100644 db/migrate/20160225050317_add_user_options.rb diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index fd702adff..a26f5e10e 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -147,25 +147,29 @@ const User = RestModel.extend({ 'location', 'name', 'locale', - 'email_digests', - 'email_direct', - 'email_always', - 'email_private_messages', - 'dynamic_favicon', - 'digest_after_days', 'new_topic_duration_minutes', - 'external_links_in_new_tab', - 'mailing_list_mode', - 'enable_quoting', - 'disable_jump_reply', 'custom_fields', 'user_fields', 'muted_usernames', 'profile_background', - 'card_background', - 'automatically_unpin_topics' + 'card_background' ); + [ 'email_always', + 'mailing_list_mode', + 'external_links_in_new_tab', + 'email_digests', + 'email_direct', + 'email_private_messages', + 'dynamic_favicon', + 'enable_quoting', + 'disable_jump_reply', + 'automatically_unpin_topics', + 'digest_after_days' + ].forEach(s => { + data[s] = this.get(`user_option.${s}`); + }); + ['muted','watched','tracked'].forEach(s => { let cats = this.get(s + 'Categories').map(c => c.get('id')); // HACK: denote lack of categories @@ -174,7 +178,7 @@ const User = RestModel.extend({ }); if (!Discourse.SiteSettings.edit_history_visible_to_public) { - data['edit_history_public'] = this.get('edit_history_public'); + data['edit_history_public'] = this.get('user_option.edit_history_public'); } // TODO: We can remove this when migrated fully to rest model. @@ -184,7 +188,7 @@ const User = RestModel.extend({ type: 'PUT' }).then(result => { this.set('bio_excerpt', result.user.bio_excerpt); - const userProps = this.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); + const userProps = Em.getProperties(this.get('user-option'),'enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); }).finally(() => { this.set('isSaving', false); diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 636227b75..4c65524e8 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -169,17 +169,17 @@ <div class="control-group pref-email-settings"> <label class="control-label">{{i18n 'user.email_settings'}}</label> {{#if canReceiveDigest}} - {{preference-checkbox labelKey="user.email_digests.title" checked=model.email_digests}} - {{#if model.email_digests}} + {{preference-checkbox labelKey="user.email_digests.title" checked=model.user_option.email_digests}} + {{#if model.user_option.email_digests}} <div class='controls controls-dropdown'> - {{combo-box valueAttribute="value" content=digestFrequencies value=model.digest_after_days}} + {{combo-box valueAttribute="value" content=digestFrequencies value=model.user_option.digest_after_days}} </div> {{/if}} {{/if}} - {{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}} - {{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}} - <span class="pref-mailing-list-mode">{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}</span> - {{preference-checkbox labelKey="user.email_always" checked=model.email_always}} + {{preference-checkbox labelKey="user.email_private_messages" checked=model.user_option.email_private_messages}} + {{preference-checkbox labelKey="user.email_direct" checked=model.user_option.email_direct}} + <span class="pref-mailing-list-mode">{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}}</span> + {{preference-checkbox labelKey="user.email_always" checked=model.user_option.email_always}} <div class='instructions'> {{#if siteSettings.email_time_window_mins}} @@ -209,12 +209,12 @@ {{combo-box valueAttribute="value" content=autoTrackDurations value=model.auto_track_topics_after_msecs}} </div> - {{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.external_links_in_new_tab}} - {{preference-checkbox labelKey="user.enable_quoting" checked=model.enable_quoting}} - {{preference-checkbox labelKey="user.dynamic_favicon" checked=model.dynamic_favicon}} - {{preference-checkbox labelKey="user.disable_jump_reply" checked=model.disable_jump_reply}} + {{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab}} + {{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}} + {{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}} + {{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}} {{#unless siteSettings.edit_history_visible_to_public}} - {{preference-checkbox labelKey="user.edit_history_public" checked=model.edit_history_public}} + {{preference-checkbox labelKey="user.edit_history_public" checked=model.user_option.edit_history_public}} {{/unless}} {{plugin-outlet "user-custom-preferences"}} @@ -254,7 +254,7 @@ {{#if siteSettings.automatically_unpin_topics}} <div class="control-group topics"> <label class="control-label">{{i18n 'categories.topics'}}</label> - {{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.automatically_unpin_topics}} + {{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}} </div> {{/if}} diff --git a/app/controllers/email_controller.rb b/app/controllers/email_controller.rb index 4e0ff301a..22bbec051 100644 --- a/app/controllers/email_controller.rb +++ b/app/controllers/email_controller.rb @@ -25,9 +25,12 @@ class EmailController < ApplicationController end if params[:from_all] - @user.update_columns(email_digests: false, email_direct: false, email_private_messages: false, email_always: false) + @user.user_option.update_columns(email_always: false, + email_digests: false, + email_direct: false, + email_private_messages: false) else - @user.update_column(:email_digests, false) + @user.user_option.update_column(:email_digests, false) end @success = true @@ -36,7 +39,7 @@ class EmailController < ApplicationController def resubscribe @user = DigestUnsubscribeKey.user_for_key(params[:key]) raise Discourse::NotFound unless @user.present? - @user.update_column(:email_digests, true) + @user.user_option.update_column(:email_digests, true) end end diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index c78e78f3e..29436e423 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -11,7 +11,8 @@ module Jobs users = User.activated.not_blocked.not_suspended.real - .where(mailing_list_mode: true) + .joins(:user_option) + .where(user_options: {mailing_list_mode: true}) .where('NOT EXISTS( SELECT 1 FROM topic_users tu diff --git a/app/jobs/regular/user_email.rb b/app/jobs/regular/user_email.rb index 6830ec2e6..ff38409c1 100644 --- a/app/jobs/regular/user_email.rb +++ b/app/jobs/regular/user_email.rb @@ -62,7 +62,7 @@ module Jobs return if user.staged && type == :digest seen_recently = (user.last_seen_at.present? && user.last_seen_at > SiteSetting.email_time_window_mins.minutes.ago) - seen_recently = false if user.email_always || user.staged + seen_recently = false if user.user_option.email_always || user.staged email_args = {} @@ -85,14 +85,14 @@ module Jobs email_args[:notification_type] = email_args[:notification_type].to_s end - if user.mailing_list_mode? && + if user.user_option.mailing_list_mode? && !post.topic.private_message? && NOTIFICATIONS_SENT_BY_MAILING_LIST.include?(email_args[:notification_type]) # no need to log a reason when the mail was already sent via the mailing list job return [nil, nil] end - unless user.email_always? + unless user.user_option.email_always? if (notification && notification.read?) || (post && post.seen?(user)) return skip_message(I18n.t('email_log.notification_already_read')) end diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb index 40cf0976d..ea74f98ed 100644 --- a/app/jobs/scheduled/enqueue_digest_emails.rb +++ b/app/jobs/scheduled/enqueue_digest_emails.rb @@ -15,8 +15,10 @@ module Jobs def target_user_ids # Users who want to receive emails and haven't been emailed in the last day query = User.real - .where(email_digests: true, active: true, staged: false) + .where(active: true, staged: false) + .joins(:user_option) .not_suspended + .where(user_options: {email_digests: true}) .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.delete_digest_email_after_days})") diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 2f4423cb1..52db55f8a 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -321,7 +321,7 @@ class UserNotifications < ActionMailer::Base context: context, username: username, add_unsubscribe_link: !user.staged, - add_unsubscribe_via_email_link: user.mailing_list_mode, + add_unsubscribe_via_email_link: user.user_option.mailing_list_mode, unsubscribe_url: post.topic.unsubscribe_url, allow_reply_by_email: allow_reply_by_email, use_site_subject: use_site_subject, diff --git a/app/models/user.rb b/app/models/user.rb index a1be4c16b..5634cbeaf 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -38,6 +38,7 @@ class User < ActiveRecord::Base has_many :user_archived_messages, dependent: :destroy + has_one :user_option, dependent: :destroy has_one :user_avatar, dependent: :destroy has_one :facebook_user_info, dependent: :destroy has_one :twitter_user_info, dependent: :destroy @@ -80,6 +81,7 @@ class User < ActiveRecord::Base after_create :create_email_token after_create :create_user_stat + after_create :create_user_option after_create :create_user_profile after_create :ensure_in_trust_level_group after_create :automatic_group_membership @@ -123,7 +125,7 @@ class User < ActiveRecord::Base # TODO-PERF: There is no indexes on any of these # and NotifyMailingListSubscribers does a select-all-and-loop - # may want to create an index on (active, blocked, suspended_till, mailing_list_mode)? + # may want to create an index on (active, blocked, suspended_till)? scope :blocked, -> { where(blocked: true) } scope :not_blocked, -> { where(blocked: false) } scope :suspended, -> { where('suspended_till IS NOT NULL AND suspended_till > ?', Time.zone.now) } @@ -911,6 +913,10 @@ class User < ActiveRecord::Base stat.save! end + def create_user_option + UserOption.create(user_id: id) + end + def create_email_token email_tokens.create(email: email) end @@ -965,21 +971,8 @@ class User < ActiveRecord::Base end def set_default_user_preferences - set_default_email_digest_frequency - set_default_email_private_messages - set_default_email_direct - set_default_email_mailing_list_mode - set_default_email_always - set_default_other_new_topic_duration_minutes set_default_other_auto_track_topics_after_msecs - set_default_other_external_links_in_new_tab - set_default_other_enable_quoting - set_default_other_dynamic_favicon - set_default_other_disable_jump_reply - set_default_other_edit_history_public - - set_default_topics_automatic_unpin # needed, otherwise the callback chain is broken... true @@ -1031,26 +1024,6 @@ class User < ActiveRecord::Base end end - def set_default_email_digest_frequency - if has_attribute?(:email_digests) - if SiteSetting.default_email_digest_frequency.to_i <= 0 - self.email_digests = false - else - self.email_digests = true - self.digest_after_days ||= SiteSetting.default_email_digest_frequency.to_i if has_attribute?(:digest_after_days) - end - end - end - - def set_default_email_mailing_list_mode - self.mailing_list_mode = SiteSetting.default_email_mailing_list_mode if has_attribute?(:mailing_list_mode) - end - - %w{private_messages direct always}.each do |s| - define_method("set_default_email_#{s}") do - self.send("email_#{s}=", SiteSetting.send("default_email_#{s}")) if has_attribute?("email_#{s}") - end - end %w{new_topic_duration_minutes auto_track_topics_after_msecs}.each do |s| define_method("set_default_other_#{s}") do @@ -1058,16 +1031,6 @@ class User < ActiveRecord::Base end end - %w{external_links_in_new_tab enable_quoting dynamic_favicon disable_jump_reply edit_history_public}.each do |s| - define_method("set_default_other_#{s}") do - self.send("#{s}=", SiteSetting.send("default_other_#{s}")) if has_attribute?(s) - end - end - - def set_default_topics_automatic_unpin - self.automatically_unpin_topics = SiteSetting.default_topics_automatic_unpin - end - end # == Schema Information @@ -1090,14 +1053,11 @@ end # last_seen_at :datetime # admin :boolean default(FALSE), not null # last_emailed_at :datetime -# email_digests :boolean not null # trust_level :integer not null # email_private_messages :boolean default(TRUE) -# email_direct :boolean default(TRUE), not null # approved :boolean default(FALSE), not null # approved_by_id :integer # approved_at :datetime -# digest_after_days :integer # previous_visit_at :datetime # suspended_at :datetime # suspended_till :datetime @@ -1107,24 +1067,16 @@ end # flag_level :integer default(0), not null # ip_address :inet # new_topic_duration_minutes :integer -# external_links_in_new_tab :boolean not null -# enable_quoting :boolean default(TRUE), not null # moderator :boolean default(FALSE) # blocked :boolean default(FALSE) -# dynamic_favicon :boolean default(FALSE), not null # title :string(255) # uploaded_avatar_id :integer -# email_always :boolean default(FALSE), not null -# mailing_list_mode :boolean default(FALSE), not null # primary_group_id :integer # locale :string(10) # registration_ip_address :inet # last_redirected_to_top_at :datetime -# disable_jump_reply :boolean default(FALSE), not null -# edit_history_public :boolean default(FALSE), not null # trust_level_locked :boolean default(FALSE), not null # staged :boolean default(FALSE), not null -# automatically_unpin_topics :boolean default(TRUE) # # Indexes # diff --git a/app/models/user_email_observer.rb b/app/models/user_email_observer.rb index 31128235e..d153d01b5 100644 --- a/app/models/user_email_observer.rb +++ b/app/models/user_email_observer.rb @@ -64,12 +64,12 @@ class UserEmailObserver < ActiveRecord::Observer EMAILABLE_POST_TYPES ||= Set.new [Post.types[:regular], Post.types[:whisper]] def enqueue(type, delay=default_delay) - return unless notification.user.email_direct? + return unless notification.user.user_option.email_direct? perform_enqueue(type, delay) end def enqueue_private(type, delay=private_delay) - return unless notification.user.email_private_messages? + return unless notification.user.user_option.email_private_messages? perform_enqueue(type, delay) end diff --git a/app/models/user_option.rb b/app/models/user_option.rb new file mode 100644 index 000000000..3dac2ded9 --- /dev/null +++ b/app/models/user_option.rb @@ -0,0 +1,29 @@ +class UserOption < ActiveRecord::Base + self.primary_key = :user_id + belongs_to :user + before_create :set_defaults + + def set_defaults + self.email_always = SiteSetting.default_email_always + self.mailing_list_mode = SiteSetting.default_email_mailing_list_mode + self.email_direct = SiteSetting.default_email_direct + self.automatically_unpin_topics = SiteSetting.default_topics_automatic_unpin + self.email_private_messages = SiteSetting.default_email_private_messages + + self.enable_quoting = SiteSetting.default_other_enable_quoting + self.external_links_in_new_tab = SiteSetting.default_other_external_links_in_new_tab + self.dynamic_favicon = SiteSetting.default_other_dynamic_favicon + self.disable_jump_reply = SiteSetting.default_other_disable_jump_reply + self.edit_history_public = SiteSetting.default_other_edit_history_public + + + if SiteSetting.default_email_digest_frequency.to_i <= 0 + self.email_digests = false + else + self.email_digests = true + self.digest_after_days ||= SiteSetting.default_email_digest_frequency.to_i + end + + true + end +end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index c576025cd..5cf912d47 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -31,7 +31,8 @@ class CurrentUserSerializer < BasicUserSerializer :is_anonymous, :post_queue_new_count, :show_queued_posts, - :read_faq + :read_faq, + :automatically_unpin_topics def include_site_flagged_posts_count? object.staff? @@ -49,6 +50,26 @@ class CurrentUserSerializer < BasicUserSerializer object.user_stat.topic_reply_count end + def enable_quoting + object.user_option.enable_quoting + end + + def disable_jump_reply + object.user_option.disable_jump_reply + end + + def external_links_in_new_tab + object.user_option.external_links_in_new_tab + end + + def dynamic_favicon + object.user_option.dynamic_favicon + end + + def automatically_unpin_topics + object.user_option.automatically_unpin_topics + end + def site_flagged_posts_count PostAction.flagged_posts_count end diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb new file mode 100644 index 000000000..77c9d6854 --- /dev/null +++ b/app/serializers/user_option_serializer.rb @@ -0,0 +1,20 @@ +class UserOptionSerializer < ApplicationSerializer + attributes :user_id, + :email_always, + :mailing_list_mode, + :email_digests, + :email_private_messages, + :email_direct, + :external_links_in_new_tab, + :dynamic_favicon, + :enable_quoting, + :disable_jump_reply, + :digest_after_days, + :automatically_unpin_topics, + :edit_history_public + + + def include_edit_history_public? + !SiteSetting.edit_history_visible_to_public + end +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 480fbd9c6..1ad96ce24 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -61,40 +61,33 @@ class UserSerializer < BasicUserSerializer :uploaded_avatar_id, :badge_count, :has_title_badges, - :edit_history_public, :custom_fields, :user_fields, :topic_post_count, :pending_count, - :profile_view_count, - :automatically_unpin_topics + :profile_view_count has_one :invited_by, embed: :object, serializer: BasicUserSerializer has_many :groups, embed: :object, serializer: BasicGroupSerializer has_many :featured_user_badges, embed: :ids, serializer: UserBadgeSerializer, root: :user_badges has_one :card_badge, embed: :object, serializer: BadgeSerializer + has_one :user_option, embed: :object, serializer: UserOptionSerializer + + def include_user_option? + can_edit + end staff_attributes :post_count, :can_be_deleted, :can_delete_all_posts private_attributes :locale, - :email_digests, - :email_private_messages, - :email_direct, - :email_always, - :digest_after_days, - :mailing_list_mode, :auto_track_topics_after_msecs, :new_topic_duration_minutes, - :external_links_in_new_tab, - :dynamic_favicon, - :enable_quoting, :muted_category_ids, :tracked_category_ids, :watched_category_ids, :private_messages_stats, - :disable_jump_reply, :system_avatar_upload_id, :system_avatar_template, :gravatar_avatar_upload_id, @@ -322,10 +315,6 @@ class UserSerializer < BasicUserSerializer object.badges.where(allow_title: true).count > 0 end - def include_edit_history_public? - can_edit && !SiteSetting.edit_history_visible_to_public - end - def user_fields object.user_fields end diff --git a/app/services/anonymous_shadow_creator.rb b/app/services/anonymous_shadow_creator.rb index 18b7eccea..08c1d6a33 100644 --- a/app/services/anonymous_shadow_creator.rb +++ b/app/services/anonymous_shadow_creator.rb @@ -40,11 +40,14 @@ class AnonymousShadowCreator active: true, trust_level: 1, trust_level_locked: true, - email_private_messages: false, - email_digests: false, created_at: 1.day.ago # bypass new user restrictions ) + shadow.user_option.update_columns( + email_private_messages: false, + email_digests: false + ) + shadow.email_tokens.update_all(confirmed: true) shadow.activate diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb index 48699b176..e9ff162db 100644 --- a/app/services/user_anonymizer.rb +++ b/app/services/user_anonymizer.rb @@ -23,14 +23,17 @@ class UserAnonymizer @user.name = nil @user.date_of_birth = nil @user.title = nil - @user.email_digests = false - @user.email_private_messages = false - @user.email_direct = false - @user.email_always = false - @user.mailing_list_mode = false @user.uploaded_avatar_id = nil @user.save + options = @user.user_option + options.email_always = false + options.mailing_list_mode = false + options.email_digests = false + options.email_private_messages = false + options.email_direct = false + options.save + profile = @user.user_profile profile.destroy if profile @user.create_user_profile diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index c6f236d2b..e028a0d14 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -6,18 +6,19 @@ class UserUpdater muted_category_ids: :muted } - USER_ATTR = [ - :email_digests, + OPTION_ATTR = [ :email_always, + :mailing_list_mode, + :email_digests, :email_direct, :email_private_messages, :external_links_in_new_tab, :enable_quoting, :dynamic_favicon, - :mailing_list_mode, :disable_jump_reply, :edit_history_public, :automatically_unpin_topics, + :digest_after_days ] def initialize(actor, user) @@ -36,7 +37,6 @@ class UserUpdater user.name = attributes.fetch(:name) { user.name } user.locale = attributes.fetch(:locale) { user.locale } - user.digest_after_days = attributes.fetch(:digest_after_days) { user.digest_after_days } if attributes[:auto_track_topics_after_msecs] user.auto_track_topics_after_msecs = attributes[:auto_track_topics_after_msecs].to_i @@ -56,9 +56,19 @@ class UserUpdater end end - USER_ATTR.each do |attribute| + + save_options = false + OPTION_ATTR.each do |attribute| if attributes[attribute].present? - user.send("#{attribute}=", attributes[attribute] == 'true') + save_options = true + + + if [true,false].include?(user.user_option.send(attribute)) + val = attributes[attribute].to_s == 'true' + user.user_option.send("#{attribute}=", val) + else + user.user_option.send("#{attribute}=", attributes[attribute]) + end end end @@ -72,7 +82,7 @@ class UserUpdater update_muted_users(attributes[:muted_usernames]) end - user_profile.save && user.save + (!save_options || user.user_option.save) && user_profile.save && user.save end end diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index 7effb7377..e6c7486b9 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -16,12 +16,15 @@ User.seed do |u| u.active = true u.admin = true u.moderator = true - u.email_direct = false u.approved = true - u.email_private_messages = false u.trust_level = TrustLevel[4] end +UserOption.where(user_id: -1).update_all( + email_private_messages: false, + email_direct: false +) + Group.user_trust_level_change!(-1, TrustLevel[4]) # User for the smoke tests @@ -49,3 +52,4 @@ if ENV["SMOKE"] == "1" et.confirmed = true end end + diff --git a/db/migrate/20160225050317_add_user_options.rb b/db/migrate/20160225050317_add_user_options.rb new file mode 100644 index 000000000..13df057cd --- /dev/null +++ b/db/migrate/20160225050317_add_user_options.rb @@ -0,0 +1,76 @@ +class AddUserOptions < ActiveRecord::Migration + def up + + create_table :user_options, id: false do |t| + t.integer :user_id, null: false + t.boolean :email_always, null: false, default: false + t.boolean :mailing_list_mode, null: false, default: false + t.boolean :email_digests + t.boolean :email_direct, null: false, default: true + t.boolean :email_private_messages, null: false, default: true + t.boolean :external_links_in_new_tab, null: false, default: false + t.boolean :enable_quoting, null: false, default: true + t.boolean :dynamic_favicon, null: false, default: false + t.boolean :disable_jump_reply, null: false, default: false + t.boolean :edit_history_public, null: false, default: false + t.boolean :automatically_unpin_topics, null: false, default: true + t.integer :digest_after_days + end + + add_index :user_options, [:user_id], unique: true + + execute <<SQL + INSERT INTO user_options ( + user_id, + email_always, + mailing_list_mode, + email_digests, + email_direct, + email_private_messages, + external_links_in_new_tab, + enable_quoting, + dynamic_favicon, + disable_jump_reply, + edit_history_public, + automatically_unpin_topics, + digest_after_days + ) + SELECT id, + email_always, + mailing_list_mode, + email_digests, + email_direct, + email_private_messages, + external_links_in_new_tab, + enable_quoting, + dynamic_favicon, + disable_jump_reply, + edit_history_public, + automatically_unpin_topics, + digest_after_days + FROM users +SQL + + # these can not be removed until a bit later + # if we remove them now all currently running unicorns will start erroring out + # + # remove_column :users, :email_always + # remove_column :users, :mailing_list_mode + # remove_column :users, :email_digests + # remove_column :users, :email_direct + # remove_column :users, :email_private_messages + # remove_column :users, :external_links_in_new_tab + # remove_column :users, :enable_quoting + # remove_column :users, :dynamic_favicon + # remove_column :users, :disable_jump_reply + # remove_column :users, :edit_history_public + # remove_column :users, :automatically_unpin_topics + # remove_column :users, :digest_after_days + end + + def down + # we can not move backwards here cause columns + # get removed an hour after the migration + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 722c01748..8635bc6c1 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -157,7 +157,7 @@ module PostGuardian return false unless post if !post.hidden - return true if post.wiki || SiteSetting.edit_history_visible_to_public || post.user.try(:edit_history_public) + return true if post.wiki || SiteSetting.edit_history_visible_to_public || (post.user && post.user.user_option.edit_history_public) end authenticated? && diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index e71c1550a..14db91210 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -510,7 +510,7 @@ describe Guardian do it 'is true if the author has public edit history' do public_post_revision = Fabricate(:post_revision) - public_post_revision.post.user.edit_history_public = true + public_post_revision.post.user.user_option.edit_history_public = true expect(Guardian.new.can_see?(public_post_revision)).to be_truthy end end @@ -533,7 +533,7 @@ describe Guardian do it 'is true if the author has public edit history' do public_post_revision = Fabricate(:post_revision) - public_post_revision.post.user.edit_history_public = true + public_post_revision.post.user.user_option.edit_history_public = true expect(Guardian.new.can_see?(public_post_revision)).to be_truthy end end diff --git a/spec/controllers/email_controller_spec.rb b/spec/controllers/email_controller_spec.rb index f5c6e9180..0376076b9 100644 --- a/spec/controllers/email_controller_spec.rb +++ b/spec/controllers/email_controller_spec.rb @@ -21,7 +21,11 @@ describe EmailController do context '.resubscribe' do - let(:user) { Fabricate(:user, email_digests: false) } + let(:user) { + user = Fabricate(:user) + user.user_option.update_columns(email_digests: false) + user + } let(:key) { DigestUnsubscribeKey.create_key_for(user) } context 'with a valid key' do @@ -31,7 +35,7 @@ describe EmailController do end it 'subscribes the user' do - expect(user.email_digests).to eq(true) + expect(user.user_option.email_digests).to eq(true) end end @@ -39,7 +43,11 @@ describe EmailController do context '.unsubscribe' do - let(:user) { Fabricate(:user, email_digests: true, email_direct: true, email_private_messages: true, email_always: true) } + let(:user) { + user = Fabricate(:user) + user.user_option.update_columns(email_always: true, email_digests: true, email_direct: true, email_private_messages: true) + user + } let(:key) { DigestUnsubscribeKey.create_key_for(user) } context 'from confirm unsubscribe email' do @@ -49,10 +57,11 @@ describe EmailController do end it 'unsubscribes from all emails' do - expect(user.email_digests).to eq false - expect(user.email_direct).to eq false - expect(user.email_private_messages).to eq false - expect(user.email_always).to eq false + options = user.user_option + expect(options.email_digests).to eq false + expect(options.email_direct).to eq false + expect(options.email_private_messages).to eq false + expect(options.email_always).to eq false end end @@ -63,7 +72,7 @@ describe EmailController do end it 'unsubscribes the user' do - expect(user.email_digests).to eq(false) + expect(user.user_option.email_digests).to eq(false) end it "sets the appropriate instance variables" do @@ -90,7 +99,7 @@ describe EmailController do end it 'does not unsubscribe the user' do - expect(user.email_digests).to eq(true) + expect(user.user_option.email_digests).to eq(true) end it 'sets the appropriate instance variables' do @@ -108,7 +117,7 @@ describe EmailController do end it 'unsubscribes the user' do - expect(user.email_digests).to eq(false) + expect(user.user_option.email_digests).to eq(false) end it 'sets the appropriate instance variables' do diff --git a/spec/jobs/user_email_spec.rb b/spec/jobs/user_email_spec.rb index c7ab345a1..5cce202fe 100644 --- a/spec/jobs/user_email_spec.rb +++ b/spec/jobs/user_email_spec.rb @@ -55,7 +55,8 @@ describe Jobs::UserEmail do end it "does send an email to a user that's been recently seen but has email_always set" do - user.update_attributes(last_seen_at: 9.minutes.ago, email_always: true) + user.update_attributes(last_seen_at: 9.minutes.ago) + user.user_option.update_attributes(email_always: true) Email::Sender.any_instance.expects(:send) Jobs::UserEmail.new.execute(type: :user_replied, user_id: user.id, post_id: post.id) end @@ -188,13 +189,13 @@ describe Jobs::UserEmail do it "does send the email if the notification has been seen but the user is set for email_always" do Email::Sender.any_instance.expects(:send) notification.update_column(:read, true) - user.update_column(:email_always, true) + user.user_option.update_column(:email_always, true) Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id) end it "doesn't send the mail if the user is using mailing list mode" do Email::Sender.any_instance.expects(:send).never - user.update_column(:mailing_list_mode, true) + user.user_option.update_column(:mailing_list_mode, true) # sometimes, we pass the notification_id Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id) # other times, we only pass the type of notification diff --git a/spec/models/user_email_observer_spec.rb b/spec/models/user_email_observer_spec.rb index c0b8da164..2bd23dd8e 100644 --- a/spec/models/user_email_observer_spec.rb +++ b/spec/models/user_email_observer_spec.rb @@ -51,7 +51,7 @@ describe UserEmailObserver do include_examples "enqueue" it "doesn't enqueue a job if the user has mention emails disabled" do - notification.user.expects(:email_direct?).returns(false) + notification.user.user_option.update_columns(email_direct: false) Jobs.expects(:enqueue_in).with(delay, :user_email, has_entry(type: type)).never UserEmailObserver.process_notification(notification) end @@ -61,7 +61,7 @@ describe UserEmailObserver do include_examples "enqueue" it "doesn't enqueue a job if the user has private message emails disabled" do - notification.user.expects(:email_private_messages?).returns(false) + notification.user.user_option.update_columns(email_private_messages: false) Jobs.expects(:enqueue_in).with(delay, :user_email, has_entry(type: type)).never UserEmailObserver.process_notification(notification) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 792c9ef19..5acf8bfb6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -152,15 +152,15 @@ describe User do it "is properly initialized" do expect(subject.approved_at).to be_blank expect(subject.approved_by_id).to be_blank - expect(subject.email_private_messages).to eq(true) - expect(subject.email_direct).to eq(true) end context 'after_save' do before { subject.save } - it "has an email token" do + it "has correct settings" do expect(subject.email_tokens).to be_present + expect(subject.user_option.email_private_messages).to eq(true) + expect(subject.user_option.email_direct).to eq(true) end end @@ -1246,50 +1246,62 @@ describe User do context "when user preferences are overriden" do before do - SiteSetting.stubs(:default_email_digest_frequency).returns(1) # daily - SiteSetting.stubs(:default_email_private_messages).returns(false) - SiteSetting.stubs(:default_email_direct).returns(false) - SiteSetting.stubs(:default_email_mailing_list_mode).returns(true) - SiteSetting.stubs(:default_email_always).returns(true) + SiteSetting.default_email_digest_frequency = 1 # daily + SiteSetting.default_email_private_messages = false + SiteSetting.default_email_direct = false + SiteSetting.default_email_mailing_list_mode = true + SiteSetting.default_email_always = true - SiteSetting.stubs(:default_other_new_topic_duration_minutes).returns(-1) # not viewed - SiteSetting.stubs(:default_other_auto_track_topics_after_msecs).returns(0) # immediately - SiteSetting.stubs(:default_other_external_links_in_new_tab).returns(true) - SiteSetting.stubs(:default_other_enable_quoting).returns(false) - SiteSetting.stubs(:default_other_dynamic_favicon).returns(true) - SiteSetting.stubs(:default_other_disable_jump_reply).returns(true) - SiteSetting.stubs(:default_other_edit_history_public).returns(true) + SiteSetting.default_other_new_topic_duration_minutes = -1 # not viewed + SiteSetting.default_other_auto_track_topics_after_msecs = 0 # immediately + SiteSetting.default_other_external_links_in_new_tab = true + SiteSetting.default_other_enable_quoting = false + SiteSetting.default_other_dynamic_favicon = true + SiteSetting.default_other_disable_jump_reply = true + SiteSetting.default_other_edit_history_public = true - SiteSetting.stubs(:default_topics_automatic_unpin).returns(false) + SiteSetting.default_topics_automatic_unpin = false - SiteSetting.stubs(:default_categories_watching).returns("1") - SiteSetting.stubs(:default_categories_tracking).returns("2") - SiteSetting.stubs(:default_categories_muted).returns("3") + SiteSetting.default_categories_watching = "1" + SiteSetting.default_categories_tracking = "2" + SiteSetting.default_categories_muted = "3" end it "has overriden preferences" do user = Fabricate(:user) - - expect(user.digest_after_days).to eq(1) - expect(user.email_private_messages).to eq(false) - expect(user.email_direct).to eq(false) - expect(user.mailing_list_mode).to eq(true) - expect(user.email_always).to eq(true) + options = user.user_option + expect(options.email_always).to eq(true) + expect(options.mailing_list_mode).to eq(true) + expect(options.digest_after_days).to eq(1) + expect(options.email_private_messages).to eq(false) + expect(options.external_links_in_new_tab).to eq(true) + expect(options.enable_quoting).to eq(false) + expect(options.dynamic_favicon).to eq(true) + expect(options.disable_jump_reply).to eq(true) + expect(options.edit_history_public).to eq(true) + expect(options.automatically_unpin_topics).to eq(false) + expect(options.email_direct).to eq(false) expect(user.new_topic_duration_minutes).to eq(-1) expect(user.auto_track_topics_after_msecs).to eq(0) - expect(user.external_links_in_new_tab).to eq(true) - expect(user.enable_quoting).to eq(false) - expect(user.dynamic_favicon).to eq(true) - expect(user.disable_jump_reply).to eq(true) - expect(user.edit_history_public).to eq(true) - - expect(user.automatically_unpin_topics).to eq(false) expect(CategoryUser.lookup(user, :watching).pluck(:category_id)).to eq([1]) expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([2]) expect(CategoryUser.lookup(user, :muted).pluck(:category_id)).to eq([3]) end + end + + context UserOption do + + it "Creates a UserOption row when a user record is created and destroys once done" do + user = Fabricate(:user) + expect(user.user_option.email_always).to eq(false) + + user_id = user.id + user.destroy! + expect(UserOption.find_by(user_id: user_id)).to eq(nil) + + end end diff --git a/spec/serializers/user_serializer_spec.rb b/spec/serializers/user_serializer_spec.rb index 7f16958a2..3a9db1a23 100644 --- a/spec/serializers/user_serializer_spec.rb +++ b/spec/serializers/user_serializer_spec.rb @@ -15,6 +15,24 @@ describe UserSerializer do end end + context "as current user" do + it "serializes options correctly" do + # so we serialize more stuff + SiteSetting.edit_history_visible_to_public = false + + user = Fabricate.build(:user, + user_profile: Fabricate.build(:user_profile), + user_option: UserOption.new(edit_history_public: true), + user_stat: UserStat.new + ) + + json = UserSerializer.new(user, scope: Guardian.new(user), root: false).as_json + + expect(json[:user_option][:edit_history_public]).to eq(true) + + end + end + context "with a user" do let(:user) { Fabricate.build(:user, user_profile: Fabricate.build(:user_profile) ) } let(:serializer) { UserSerializer.new(user, scope: Guardian.new, root: false) } @@ -26,7 +44,7 @@ describe UserSerializer do context "with `enable_names` true" do before do - SiteSetting.stubs(:enable_names?).returns(true) + SiteSetting.enable_names = true end it "has a name" do @@ -34,6 +52,7 @@ describe UserSerializer do end end + context "with `enable_names` false" do before do SiteSetting.stubs(:enable_names?).returns(false) diff --git a/spec/services/anonymous_shadow_creator_spec.rb b/spec/services/anonymous_shadow_creator_spec.rb index 1483f59c7..bda16a782 100644 --- a/spec/services/anonymous_shadow_creator_spec.rb +++ b/spec/services/anonymous_shadow_creator_spec.rb @@ -30,6 +30,9 @@ describe AnonymousShadowCreator do freeze_time 4.minutes.from_now shadow3 = AnonymousShadowCreator.get(user) + expect(shadow3.user_option.email_digests).to eq(false) + expect(shadow3.user_option.email_private_messages).to eq(false) + expect(shadow2.id).not_to eq(shadow3.id) end diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb index b641c1c04..9cef2507f 100644 --- a/spec/services/user_anonymizer_spec.rb +++ b/spec/services/user_anonymizer_spec.rb @@ -19,13 +19,17 @@ describe UserAnonymizer do end it "turns off all notifications" do + user.user_option.update_columns( + email_always: true + ) + make_anonymous user.reload - expect(user.email_digests).to eq(false) - expect(user.email_private_messages).to eq(false) - expect(user.email_direct).to eq(false) - expect(user.email_always).to eq(false) - expect(user.mailing_list_mode).to eq(false) + expect(user.user_option.email_digests).to eq(false) + expect(user.user_option.email_private_messages).to eq(false) + expect(user.user_option.email_direct).to eq(false) + expect(user.user_option.email_always).to eq(false) + expect(user.user_option.mailing_list_mode).to eq(false) end it "resets profile to default values" do diff --git a/spec/services/user_updater_spec.rb b/spec/services/user_updater_spec.rb index 1f46e5eb5..f94d2bb9e 100644 --- a/spec/services/user_updater_spec.rb +++ b/spec/services/user_updater_spec.rb @@ -32,26 +32,33 @@ describe UserUpdater do describe '#update' do it 'saves user' do user = Fabricate(:user, name: 'Billy Bob') - updater = described_class.new(acting_user, user) + updater = UserUpdater.new(acting_user, user) updater.update(name: 'Jim Tom') expect(user.reload.name).to eq 'Jim Tom' end - it 'updates bio' do + it 'updates various fields' do user = Fabricate(:user) - updater = described_class.new(acting_user, user) + updater = UserUpdater.new(acting_user, user) - updater.update(bio_raw: 'my new bio') + updater.update(bio_raw: 'my new bio', + email_always: 'true', + mailing_list_mode: true, + digest_after_days: "8") + user.reload - expect(user.reload.user_profile.bio_raw).to eq 'my new bio' + expect(user.user_profile.bio_raw).to eq 'my new bio' + expect(user.user_option.email_always).to eq true + expect(user.user_option.mailing_list_mode).to eq true + expect(user.user_option.digest_after_days).to eq 8 end context 'when update succeeds' do it 'returns true' do user = Fabricate(:user) - updater = described_class.new(acting_user, user) + updater = UserUpdater.new(acting_user, user) expect(updater.update).to be_truthy end @@ -61,7 +68,7 @@ describe UserUpdater do it 'returns false' do user = Fabricate(:user) user.stubs(save: false) - updater = described_class.new(acting_user, user) + updater = UserUpdater.new(acting_user, user) expect(updater.update).to be_falsey end @@ -73,7 +80,7 @@ describe UserUpdater do guardian = stub guardian.stubs(:can_grant_title?).with(user).returns(true) Guardian.stubs(:new).with(acting_user).returns(guardian) - updater = described_class.new(acting_user, user) + updater = UserUpdater.new(acting_user, user) updater.update(title: 'Minion') From bbbb09a6fb1f09e1296543eb72ab80179c221856 Mon Sep 17 00:00:00 2001 From: Sam <sam.saffron@gmail.com> Date: Wed, 17 Feb 2016 17:47:47 +1100 Subject: [PATCH 5/6] FEATURE: start tracking information about migrations that run This commit adds a new tracking table that lets us know - When a migration ran - What version Discourse was at - How long it took - What version Rails was at The built in tracking in Rails is very limited, does not track this info --- ...0225050318_add_schema_migration_details.rb | 30 +++++++++++ .../schema_migration_details.rb | 54 +++++++++++++++++++ .../schema_migration_details_spec.rb | 32 +++++++++++ 3 files changed, 116 insertions(+) create mode 100644 db/migrate/20000225050318_add_schema_migration_details.rb create mode 100644 lib/freedom_patches/schema_migration_details.rb create mode 100644 spec/components/freedom_patches/schema_migration_details_spec.rb diff --git a/db/migrate/20000225050318_add_schema_migration_details.rb b/db/migrate/20000225050318_add_schema_migration_details.rb new file mode 100644 index 000000000..dbf4959b1 --- /dev/null +++ b/db/migrate/20000225050318_add_schema_migration_details.rb @@ -0,0 +1,30 @@ +class AddSchemaMigrationDetails < ActiveRecord::Migration + def up + # schema_migrations table is way too thin, does not give info about + # duration of migration or the date it happened, this migration together with the + # monkey patch adds a lot of information to the migration table + + create_table :schema_migration_details do |t| + t.string :version, null: false + t.string :name + t.string :hostname + t.string :git_version + t.string :rails_version + t.integer :duration + t.string :direction # this really should be a pg enum type but annoying to wire up for little gain + t.datetime :created_at, null: false + end + + add_index :schema_migration_details, [:version] + + execute("INSERT INTO schema_migration_details(version, created_at) + SELECT version, current_timestamp + FROM schema_migrations + ORDER BY version + ") + end + + def down + drop_table :schema_migration_details + end +end diff --git a/lib/freedom_patches/schema_migration_details.rb b/lib/freedom_patches/schema_migration_details.rb new file mode 100644 index 000000000..bbae956c1 --- /dev/null +++ b/lib/freedom_patches/schema_migration_details.rb @@ -0,0 +1,54 @@ +module FreedomPatches + module SchemaMigrationDetails + def exec_migration(conn, direction) + rval = nil + + time = Benchmark.measure do + rval=super + end + + sql = <<SQL + INSERT INTO schema_migration_details( + version, + hostname, + name, + git_version, + duration, + direction, + rails_version, + created_at + ) values ( + :version, + :hostname, + :name, + :git_version, + :duration, + :direction, + :rails_version, + :created_at + ) +SQL + + hostname = `hostname` rescue "" + sql = ActiveRecord::Base.send(:sanitize_sql_array, [sql, { + version: version || "", + duration: (time.real * 1000).to_i, + hostname: hostname, + name: name, + git_version: Discourse.git_version, + created_at: Time.zone.now, + direction: direction.to_s, + rails_version: Rails.version + }]) + + conn.execute(sql) + + rval + end + + end +end + +class ActiveRecord::Migration + prepend FreedomPatches::SchemaMigrationDetails +end diff --git a/spec/components/freedom_patches/schema_migration_details_spec.rb b/spec/components/freedom_patches/schema_migration_details_spec.rb new file mode 100644 index 000000000..86e960edb --- /dev/null +++ b/spec/components/freedom_patches/schema_migration_details_spec.rb @@ -0,0 +1,32 @@ +require 'rails_helper' +require_dependency "freedom_patches/schema_migration_details" + +describe FreedomPatches::SchemaMigrationDetails do + + # we usually don't really need this model so lets not clutter up with it + class SchemaMigrationDetail < ActiveRecord::Base + end + + class TestMigration < ActiveRecord::Migration + def up + sleep 0.001 + end + end + + it "logs information on migration" do + migration = TestMigration.new("awesome_migration","20160225050318") + + ActiveRecord::Base.connection_pool.with_connection do |conn| + migration.exec_migration(conn, :up) + end + + info = SchemaMigrationDetail.find_by(version: "20160225050318") + + expect(info.duration).to be > 0 + expect(info.git_version).to eq Discourse.git_version + expect(info.direction).to eq "up" + expect(info.rails_version).to eq Rails.version + expect(info.filename).to eq migration.filename + expect(info.name).to eq "awesome_migration" + end +end From 6912aa9fd92dbe38a1e3c1c40d1e624f368792ab Mon Sep 17 00:00:00 2001 From: Sam <sam.saffron@gmail.com> Date: Wed, 17 Feb 2016 18:08:05 +1100 Subject: [PATCH 6/6] Remove superflous columns from the users table --- db/fixtures/009_users.rb | 34 +++++++++++++++++++ ...225050318_allow_defaults_on_users_table.rb | 10 ++++++ 2 files changed, 44 insertions(+) create mode 100644 db/migrate/20160225050318_allow_defaults_on_users_table.rb diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index e6c7486b9..4c5f11875 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -27,6 +27,40 @@ UserOption.where(user_id: -1).update_all( Group.user_trust_level_change!(-1, TrustLevel[4]) +# 60 minutes after our migration runs we need to exectue this code... +duration = Rails.env.production? ? 60 : 0 +if User.exec_sql("SELECT 1 FROM schema_migration_details + WHERE EXISTS( + SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'users' AND column_name = 'enable_quoting' + ) AND + name = 'AllowDefaultsOnUsersTable' AND + created_at < (current_timestamp at time zone 'UTC' - interval '#{duration} minutes') + ").to_a.length > 0 + + + User.transaction do + STDERR.puts "Removing superflous user columns!" + %w[ + email_always + mailing_list_mode + email_digests + email_direct + email_private_messages + external_links_in_new_tab + enable_quoting + dynamic_favicon + disable_jump_reply + edit_history_public + automatically_unpin_topics + digest_after_days + ].each do |column| + User.exec_sql("ALTER TABLE users DROP column #{column}") + end + + end +end + # User for the smoke tests if ENV["SMOKE"] == "1" smoke_user = User.seed do |u| diff --git a/db/migrate/20160225050318_allow_defaults_on_users_table.rb b/db/migrate/20160225050318_allow_defaults_on_users_table.rb new file mode 100644 index 000000000..999e275fd --- /dev/null +++ b/db/migrate/20160225050318_allow_defaults_on_users_table.rb @@ -0,0 +1,10 @@ +class AllowDefaultsOnUsersTable < ActiveRecord::Migration + def up + # we need to temporarily change table a bit to ensure we can insert new records + change_column :users, :email_digests, :boolean, null: false, default: true + change_column :users, :external_links_in_new_tab, :boolean, null: false, default: false + end + + def down + end +end