diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
index c429d6b4a2a..789b35aa374 100644
--- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6
+++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6
@@ -93,3 +93,30 @@ export function withPluginApi(version, apiCodeCallback, opts) {
return apiCodeCallback(api);
}
}
+
+// 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();
+ }
+}
diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6
index fd702adff76..a26f5e10e7a 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 636227b757b..4c65524e82b 100644
--- a/app/assets/javascripts/discourse/templates/user/preferences.hbs
+++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs
@@ -169,17 +169,17 @@
{{#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}}
- {{combo-box valueAttribute="value" content=digestFrequencies value=model.digest_after_days}}
+ {{combo-box valueAttribute="value" content=digestFrequencies value=model.user_option.digest_after_days}}
{{/if}}
{{/if}}
- {{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}}
- {{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}}
-
{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}
- {{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}}
+
{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.user_option.mailing_list_mode}}
+ {{preference-checkbox labelKey="user.email_always" checked=model.user_option.email_always}}
{{#if siteSettings.email_time_window_mins}}
@@ -209,12 +209,12 @@
{{combo-box valueAttribute="value" content=autoTrackDurations value=model.auto_track_topics_after_msecs}}
- {{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}}
- {{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}}
{{/if}}
diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb
index 9d872c99842..ccbb3eaf692 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/controllers/email_controller.rb b/app/controllers/email_controller.rb
index 4e0ff301af7..22bbec05103 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 c78e78f3e63..29436e42327 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 6830ec2e64a..ff38409c1be 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 40cf0976d95..ea74f98ed28 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 51729427816..52db55f8ab6 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
@@ -314,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 a1be4c16b97..5634cbeaf38 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 db9dbdc73ef..d153d01b504 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
@@ -60,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 00000000000..3dac2ded9e0
--- /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 c576025cd5c..5cf912d4734 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 00000000000..77c9d68540a
--- /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 480fbd9c612..1ad96ce2405 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 18b7ecceaf3..08c1d6a3380 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 48699b176a7..e9ff162dbc6 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 c6f236d2b32..e028a0d14b0 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/config/locales/server.en.yml b/config/locales/server.en.yml
index 78d65d38490..5a4011301a5 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -2092,6 +2092,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/db/fixtures/009_users.rb b/db/fixtures/009_users.rb
index 7effb73775a..4c5f1187540 100644
--- a/db/fixtures/009_users.rb
+++ b/db/fixtures/009_users.rb
@@ -16,14 +16,51 @@ 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])
+# 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|
@@ -49,3 +86,4 @@ if ENV["SMOKE"] == "1"
et.confirmed = true
end
end
+
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 00000000000..dbf4959b173
--- /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/db/migrate/20160225050317_add_user_options.rb b/db/migrate/20160225050317_add_user_options.rb
new file mode 100644
index 00000000000..13df057cda4
--- /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 <
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
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index e71c1550a91..14db91210fc 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 f5c6e918090..0376076b9f9 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 c7ab345a11a..5cce202fe38 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 0f1150c4dd1..2bd23dd8e18 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
@@ -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 }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 792c9ef191a..5acf8bfb6d0 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 7f16958a263..3a9db1a231b 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 1483f59c747..bda16a7824b 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 b641c1c0451..9cef2507fce 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 1f46e5eb51c..f94d2bb9e75 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')