devise-multi_email 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ae0a4824abdf90e63b3a5027520c2d4ef6465e31
4
- data.tar.gz: ae37353431e7e16eb565a0ba27c814020f644f3e
3
+ metadata.gz: 9ba4375a3b35995bdfa11564cc3f2a4e879d9731
4
+ data.tar.gz: 32c1c262c80fbfa87ef8819e40e0821ecc8ce822
5
5
  SHA512:
6
- metadata.gz: 232a734290ad0eab39000b8ecb76a3c6fc320ece267fdd43a52a1dadf726ee50cd97dae4c712de750a757ca48bbf72e9b7d6fc59b3e138b32414ee6b9d915e06
7
- data.tar.gz: 658d6b2220f1eaf9ca811d4df1b669a938cb7a2911c803abc5546eec9def051756511228eff406b282d7e088084fd8610b7950b88f6d71f2ded2876a187e59f2
6
+ metadata.gz: 9926f6af62664391396203089a60f9a0876d8b3548898ee5148626e963d4c5bd818ff8db4d20c934cf3d7672f3886274fe68e75465a08cf1fa560b84060cdd61
7
+ data.tar.gz: 711a2674cee47fb189a46220adf4d32b334609f74ad490016bc4cd87d387ac9c836b5e1eae2165d5478d747e6b0c58971b75c54bad3f522487ca47de349ac063
@@ -1,8 +1,13 @@
1
+ ### 2.0.1 - 2017-05-16
2
+
3
+ * Refactored to simplify some logic and start moving toward mimicking default Devise lifecycle behavior
4
+ * Added `Devise::MultiEmail.only_login_with_primary_email` option to restrict login to only primary emails
5
+ * Added `Devise::MultiEmail.autosave_emails` option to automatically enable `autosave` on "emails" association
6
+
1
7
  ### 2.0.0 - 2017-05-12
2
8
 
3
- * New `Devise::MultiEmail#configure` setup with options for `user` and `emails` associations and `primary_email` method names
9
+ * New `Devise::MultiEmail#configure` setup with options for `user` and `emails` associations and `primary_email_record` method names
4
10
  * Refactor to expose `_multi_email_*` prefixed methods on models
5
- * New `primary_email` method to get primary email record (however, can be configured as `primary_email_record` for backwards-compatibility)
6
11
  * Changed logic when changing an email address to look up existing email record, otherwise creating a new one, then marking it "primary"
7
12
  * Changed logic when changing an email address to mark all others as `primary = false`
8
13
  * Changed logic when changing an email address to `nil` to mark as `primary = false` rather than deleting records
data/README.md CHANGED
@@ -49,6 +49,31 @@ create_table :emails do |t|
49
49
  end
50
50
  ```
51
51
 
52
+ You can choose whether or not users can login with an email address that is not the primary email address.
53
+
54
+ ```ruby
55
+ Devise::MultiEmail.configure do |config|
56
+ # Default is `false`
57
+ config.only_login_with_primary_email = true
58
+ end
59
+ ```
60
+
61
+ The `autosave` is automatically enabled on the `emails` association by default. This is to ensure the `primary`
62
+ flag is persisted for all emails when the primary email is changed. When `autosave` is not enabled on the association,
63
+ only new emails are saved when the parent (e.g. `User`) record is saved. (Updates to already-persisted email records
64
+ are not saved.)
65
+
66
+ If you don't want `autosave` to be enabled automatically, you can disable this feature. What this will do is
67
+ enable alternative behavior, which adds an `after_save` callback to the parent record and calls `email.save` on each email
68
+ record where the `primary` value has changed.
69
+
70
+ ```ruby
71
+ Devise::MultiEmail.configure do |config|
72
+ # Default is `true`
73
+ config.autosave_emails = false
74
+ end
75
+ ```
76
+
52
77
  ### Configure custom association names
53
78
 
54
79
  You may not want to use the association `user.emails` or `email.users`. You can customize the name of the associations used. Add your custom configurations to an initializer file such as `config/initializers/devise-multi_email.rb`.
@@ -61,9 +86,8 @@ Devise::MultiEmail.configure do |config|
61
86
  config.parent_association_name = :team
62
87
  # Default is :emails for parent (e.g. User) model
63
88
  config.emails_association_name = :email_addresses
64
- # For backwards-compatibility, specify :primary_email_record
65
- # Default is :primary_email
66
- config.primary_email_method_name = :primary_email_record
89
+ # Default is :primary_email_record
90
+ config.primary_email_method_name = :primary_email
67
91
  end
68
92
 
69
93
  # Example use of custom association names
@@ -6,8 +6,8 @@ require 'devise/multi_email/version'
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'devise-multi_email'
8
8
  spec.version = Devise::MultiEmail::VERSION
9
- spec.authors = ['ALLEN WANG QIANG']
10
- spec.email = ['rovingbreeze@gmail.com']
9
+ spec.authors = ['ALLEN WANG QIANG', 'Joel Van Horn']
10
+ spec.email = ['rovingbreeze@gmail.com', 'joel@joelvanhorn.com']
11
11
 
12
12
  spec.summary = %q{Let devise support multiple emails.}
13
13
  spec.description = %q{Devise authenticatable, confirmable and validatable with multiple emails.}
@@ -3,32 +3,54 @@ require 'devise'
3
3
 
4
4
  module Devise
5
5
  module MultiEmail
6
- def self.configure(&block)
7
- yield self
8
- end
6
+ class << self
7
+ def configure(&block)
8
+ yield self
9
+ end
9
10
 
10
- def self.parent_association_name
11
- @parent_association_name ||= :user
12
- end
11
+ @autosave_emails = false
13
12
 
14
- def self.parent_association_name=(name)
15
- @parent_association_name = name.try(:to_sym)
16
- end
13
+ def autosave_emails?
14
+ @autosave_emails == true
15
+ end
17
16
 
18
- def self.emails_association_name
19
- @emails_association_name ||= :emails
20
- end
17
+ def autosave_emails=(value)
18
+ @autosave_emails = (value == true)
19
+ end
21
20
 
22
- def self.emails_association_name=(name)
23
- @emails_association_name = name.try(:to_sym)
24
- end
21
+ @only_login_with_primary_email = false
25
22
 
26
- def self.primary_email_method_name
27
- @primary_email_method_name ||= :primary_email
28
- end
23
+ def only_login_with_primary_email?
24
+ @only_login_with_primary_email == true
25
+ end
26
+
27
+ def only_login_with_primary_email=(value)
28
+ @only_login_with_primary_email = (value == true)
29
+ end
30
+
31
+ def parent_association_name
32
+ @parent_association_name ||= :user
33
+ end
34
+
35
+ def parent_association_name=(name)
36
+ @parent_association_name = name.try(:to_sym)
37
+ end
38
+
39
+ def emails_association_name
40
+ @emails_association_name ||= :emails
41
+ end
42
+
43
+ def emails_association_name=(name)
44
+ @emails_association_name = name.try(:to_sym)
45
+ end
46
+
47
+ def primary_email_method_name
48
+ @primary_email_method_name ||= :primary_email_record
49
+ end
29
50
 
30
- def self.primary_email_method_name=(name)
31
- @primary_email_method_name = name.try(:to_sym)
51
+ def primary_email_method_name=(name)
52
+ @primary_email_method_name = name.try(:to_sym)
53
+ end
32
54
  end
33
55
  end
34
56
  end
@@ -14,16 +14,29 @@ module Devise
14
14
  model_class.__send__ :include, mod
15
15
  end
16
16
 
17
- def model_class
18
- unless reflection
19
- raise "#{@klass}##{name} association not found: It might be because your declaration is after `devise :multi_email_confirmable`."
17
+ # Specify a block with alternative behavior which should be
18
+ # run when `autosave` is not enabled.
19
+ def configure_autosave!(&block)
20
+ unless autosave_enabled?
21
+ if Devise::MultiEmail.autosave_emails?
22
+ reflection.autosave = true
23
+ else
24
+ yield if block_given?
25
+ end
20
26
  end
27
+ end
21
28
 
29
+ def autosave_enabled?
30
+ reflection.options[:autosave] == true
31
+ end
32
+
33
+ def model_class
22
34
  @model_class ||= reflection.class_name.constantize
23
35
  end
24
36
 
25
37
  def reflection
26
- @reflection ||= @klass.reflect_on_association(name)
38
+ @reflection ||= @klass.reflect_on_association(name) ||
39
+ raise("#{@klass}##{name} association not found: It might be because your declaration is after `devise :multi_email_confirmable`.")
27
40
  end
28
41
  end
29
42
  end
@@ -4,14 +4,12 @@ module Devise
4
4
  module MultiEmail
5
5
  class EmailModelManager
6
6
 
7
- attr_reader :record
8
-
9
- def initialize(record)
10
- @record = record
7
+ def initialize(email_record)
8
+ @email_record = email_record
11
9
  end
12
10
 
13
11
  def parent
14
- record.__send__(record.class.multi_email_association.name)
12
+ @email_record.__send__(@email_record.class.multi_email_association.name)
15
13
  end
16
14
  end
17
15
  end
@@ -29,6 +29,7 @@ module Devise
29
29
  extend ActiveSupport::Concern
30
30
 
31
31
  included do
32
+ multi_email_association.configure_autosave!{ include AuthenticatableAutosaveExtensions }
32
33
  multi_email_association.include_module(EmailAuthenticatable)
33
34
  end
34
35
 
@@ -36,26 +37,40 @@ module Devise
36
37
 
37
38
  # Gets the primary email address of the user.
38
39
  def email
39
- multi_email.primary_email.try(:email)
40
+ multi_email.primary_email_record.try(:email)
40
41
  end
41
42
 
42
43
  # Sets the default email address of the user.
43
44
  def email=(new_email)
44
- multi_email.change_primary_email_to(new_email)
45
+ multi_email.change_primary_email_to(new_email, allow_unconfirmed: true)
46
+ end
47
+ end
48
+
49
+ module AuthenticatableAutosaveExtensions
50
+ extend ActiveSupport::Concern
51
+
52
+ included do
53
+ # Toggle `primary` value for all emails if `autosave` is not on
54
+ after_save do
55
+ multi_email.filtered_emails.each do |email|
56
+ # update value in database without persisting any other changes
57
+ email.save if email.changes.key?(:primary)
58
+ end
59
+ end
45
60
  end
46
61
  end
47
62
 
48
63
  module ClassMethods
49
64
  def find_first_by_auth_conditions(tainted_conditions, opts = {})
50
65
  filtered_conditions = devise_parameter_filter.filter(tainted_conditions.dup)
51
- email = filtered_conditions.delete(:email)
66
+ criteria = filtered_conditions.extract!(:email, :unconfirmed_email)
52
67
 
53
- if email && email.is_a?(String)
68
+ if criteria.keys.any?
54
69
  conditions = filtered_conditions.to_h.merge(opts).
55
- reverse_merge(multi_email_association.reflection.table_name => { email: email })
70
+ reverse_merge(build_conditions(criteria))
56
71
 
57
72
  resource = joins(multi_email_association.name).find_by(conditions)
58
- resource.current_login_email = email if resource.respond_to?(:current_login_email=)
73
+ resource.current_login_email = criteria.values.first if resource
59
74
  resource
60
75
  else
61
76
  super(tainted_conditions, opts)
@@ -63,7 +78,17 @@ module Devise
63
78
  end
64
79
 
65
80
  def find_by_email(email)
66
- joins(multi_email_association.name).where(multi_email_association.reflection.table_name => { email: email.downcase }).first
81
+ joins(multi_email_association.name).where(build_conditions email: email).first
82
+ end
83
+
84
+ def build_conditions(criteria)
85
+ criteria = devise_parameter_filter.filter(criteria)
86
+ # match the primary email record if the `unconfirmed_email` column is specified
87
+ if Devise::MultiEmail.only_login_with_primary_email? || criteria[:unconfirmed_email]
88
+ criteria.merge!(primary: true)
89
+ end
90
+
91
+ { multi_email_association.reflection.table_name.to_sym => criteria }
67
92
  end
68
93
  end
69
94
  end
@@ -8,12 +8,12 @@ module Devise
8
8
  included do
9
9
  devise :confirmable
10
10
 
11
- extend ClassReplacementMethods
11
+ include ConfirmableExtensions
12
12
  end
13
13
 
14
- module ClassReplacementMethods
15
- def allow_unconfirmed_access_for
16
- 0.days
14
+ module ConfirmableExtensions
15
+ def confirmation_period_valid?
16
+ primary? ? super : false
17
17
  end
18
18
  end
19
19
  end
@@ -45,9 +45,15 @@ module Devise
45
45
  :confirmation_token, :confirmed_at, :confirmation_sent_at, :confirm, :confirmed?, :unconfirmed_email,
46
46
  :reconfirmation_required?, :pending_reconfirmation?, to: :primary_email_record, allow_nil: true
47
47
 
48
+ # In case email updates are being postponed, don't change anything
49
+ # when the postpone feature tries to switch things back
50
+ def email=(new_email)
51
+ multi_email.change_primary_email_to(new_email, allow_unconfirmed: false)
52
+ end
53
+
48
54
  # This need to be forwarded to the email that the user logged in with
49
55
  def active_for_authentication?
50
- login_email = current_login_email_record
56
+ login_email = multi_email.login_email_record
51
57
 
52
58
  if login_email && !login_email.primary?
53
59
  super && login_email.active_for_authentication?
@@ -58,7 +64,7 @@ module Devise
58
64
 
59
65
  # Shows email not confirmed instead of account inactive when the email that user used to login is not confirmed
60
66
  def inactive_message
61
- login_email = current_login_email_record
67
+ login_email = multi_email.login_email_record
62
68
 
63
69
  if login_email && !login_email.primary? && !login_email.confirmed?
64
70
  :unconfirmed
@@ -88,12 +94,6 @@ module Devise
88
94
 
89
95
  private
90
96
 
91
- def current_login_email_record
92
- if respond_to?(:current_login_email) && current_login_email
93
- multi_email.emails.find_by(email: current_login_email)
94
- end
95
- end
96
-
97
97
  module ClassMethods
98
98
  delegate :confirm_by_token, :send_confirmation_instructions, to: 'multi_email_association.model_class', allow_nil: false
99
99
  end
@@ -61,12 +61,8 @@ module Devise
61
61
 
62
62
  def propagate_email_errors
63
63
  email_error_key = self.class.multi_email_association.name
64
-
65
- if respond_to?("#{email_error_key}_attributes=")
66
- email_error_key = "#{email_error_key}.email".to_sym
67
- end
68
-
69
- email_errors = errors.delete(email_error_key) || []
64
+ email_errors = errors.delete(email_error_key) ||
65
+ errors.delete("#{email_error_key}.email".to_sym) || []
70
66
 
71
67
  email_errors.each do |error|
72
68
  errors.add(:email, error)
@@ -80,7 +76,7 @@ module Devise
80
76
  :validates_confirmation_of, :validates_length_of].freeze
81
77
 
82
78
  def assert_validations_api!(base) #:nodoc:
83
- unavailable_validations = VALIDATIONS.select{ |v| !base.respond_to?(v) }
79
+ unavailable_validations = VALIDATIONS.select { |v| !base.respond_to?(v) }
84
80
 
85
81
  unless unavailable_validations.empty?
86
82
  raise "Could not use :validatable module since #{base} does not respond " <<
@@ -8,9 +8,10 @@ module Devise
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
+ multi_email_association.configure_autosave!
11
12
  multi_email_association.include_module(EmailModelExtensions)
12
13
  end
13
-
14
+
14
15
  delegate Devise::MultiEmail.primary_email_method_name, to: :multi_email, allow_nil: false
15
16
 
16
17
  def multi_email
@@ -4,51 +4,86 @@ module Devise
4
4
  module MultiEmail
5
5
  class ParentModelManager
6
6
 
7
- attr_reader :record
7
+ def initialize(parent_record)
8
+ @parent_record = parent_record
9
+ end
8
10
 
9
- def initialize(record)
10
- @record = record
11
+ def current_email_record
12
+ login_email_record || primary_email_record
11
13
  end
12
14
 
13
- # Gets the email records that have not been deleted
14
- def filtered_emails
15
- emails.reject(&:destroyed?).reject(&:marked_for_destruction?)
15
+ def login_email_record
16
+ if @parent_record.current_login_email.present?
17
+ formatted_email = format_email(@parent_record.current_login_email)
18
+ filtered_emails.find { |item| item.email == formatted_email }
19
+ end
16
20
  end
17
21
 
18
22
  # Gets the primary email record.
19
- def primary_email
23
+ def primary_email_record
20
24
  filtered_emails.find(&:primary?)
21
25
  end
22
- alias_method Devise::MultiEmail.primary_email_method_name, :primary_email
23
-
24
- def change_primary_email_to(new_email)
25
- # Use Devise formatting settings for emails
26
- formatted_email = record.class.send(:devise_parameter_filter).filter(email: new_email)[:email]
27
-
28
- valid_emails = filtered_emails
26
+ alias_method Devise::MultiEmail.primary_email_method_name, :primary_email_record
29
27
 
28
+ # :allow_unconfirmed option sets this email record to primary
29
+ # :skip_confirmations option confirms this email record (without saving)
30
+ # @see `set_primary_record_to`
31
+ def change_primary_email_to(new_email, options = {})
30
32
  # mark none as primary when set to nil
31
33
  if new_email.nil?
32
- valid_emails.each{ |record| record.primary = false }
34
+ filtered_emails.each { |item| item.primary = false }
33
35
 
34
36
  # select or build an email record
35
37
  else
36
- record = valid_emails.find{ |record| record.email == formatted_email }
38
+ record = find_or_build_for_email(new_email)
37
39
 
38
- unless record
39
- record = emails.build(email: formatted_email)
40
- valid_emails << record
40
+ if record.try(:confirmed?) || primary_email_record.nil? || options[:allow_unconfirmed]
41
+ set_primary_record_to(record, options)
41
42
  end
42
-
43
- # toggle the selected record as primary and others as not
44
- valid_emails.each{ |other| other.primary = (other == record) }
45
43
  end
46
44
 
47
45
  record
48
46
  end
49
47
 
48
+ # Use Devise formatting settings for emails
49
+ def format_email(email)
50
+ @parent_record.class.__send__(:devise_parameter_filter).filter(email: email)[:email]
51
+ end
52
+
53
+ def find_or_build_for_email(email)
54
+ formatted_email = format_email(email)
55
+ record = filtered_emails.find { |item| item.email == formatted_email }
56
+ record || emails.build(email: formatted_email)
57
+ end
58
+
50
59
  def emails
51
- record.__send__(record.class.multi_email_association.name)
60
+ @parent_record.__send__(@parent_record.class.multi_email_association.name)
61
+ end
62
+
63
+ # Gets the email records that have not been deleted
64
+ def filtered_emails(options = {})
65
+ emails.lazy.reject(&:destroyed?).reject(&:marked_for_destruction?).to_a
66
+ end
67
+
68
+ def confirmed_emails
69
+ filtered_emails.select { |record| record.try(:confirmed?) }
70
+ end
71
+
72
+ def unconfirmed_emails
73
+ filtered_emails.reject { |record| record.try(:confirmed?) }
74
+ end
75
+
76
+ protected
77
+
78
+ # :skip_confirmations option confirms this email record (without saving)
79
+ def set_primary_record_to(record, options = {})
80
+ # Toggle primary flag for all emails
81
+ filtered_emails.each { |other| other.primary = (other.email == record.email) }
82
+
83
+ if options[:skip_confirmations]
84
+ record.try(:skip_confirmation!)
85
+ record.try(:skip_reconfirmation!)
86
+ end
52
87
  end
53
88
  end
54
89
  end
@@ -1,5 +1,5 @@
1
1
  module Devise
2
2
  module MultiEmail
3
- VERSION = "2.0.0"
3
+ VERSION = "2.0.1"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise-multi_email
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ALLEN WANG QIANG
8
+ - Joel Van Horn
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2017-05-16 00:00:00.000000000 Z
12
+ date: 2017-06-27 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: devise
@@ -111,6 +112,7 @@ dependencies:
111
112
  description: Devise authenticatable, confirmable and validatable with multiple emails.
112
113
  email:
113
114
  - rovingbreeze@gmail.com
115
+ - joel@joelvanhorn.com
114
116
  executables: []
115
117
  extensions: []
116
118
  extra_rdoc_files: []