devise_invitable 3.0.3 → 3.0.4

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
  SHA256:
3
- metadata.gz: e4a04ccc5139020fd72c805ab72b3a43468403afd18493c29bfee62093489218
4
- data.tar.gz: af2428564b8f6242988fa1c71468efc05378aa8550a28c9e6b7f122528987dad
3
+ metadata.gz: 1d7694e9f3f376c8f60a5d426144eebe986967f33b6ea016ca1f01c65fa19e32
4
+ data.tar.gz: 40faca7611e16510fe0d45f886e35f6622eb7d1cefe9016be0e93ba6d272e2e4
5
5
  SHA512:
6
- metadata.gz: '03358597db4af64754b4c0f38837616762e0f3db4df414910f8989d42294e09ab17f342980a0650a80b18b73a761f78fadf21d84e34d997de0c79bfc880eb8dd'
7
- data.tar.gz: 1162a02fac4275ca04dbb96a23b2f7e6db541ba1b3780ba6b6a25fbb067bba8dea534bd03ff15a45338a7c579e3eb967fb2ce4a24cb8e1afb4406508b7dd3708
6
+ metadata.gz: aab0c7da6b34b49475014c55ace27804cff1c95b15012a3209c0cf7f7e42e6612647dbdeb0ddacd8fd6e26353b2617355f8c4e2f25a3dd8a2e4aaf9aa0b89d7e
7
+ data.tar.gz: 2d7873782c77d7c9076312eea7a57cb8cfc4e54874dbd4705e6133d1a5ed92f981291ec8bc7bd81cf4570836059c2b2aff6fffdf01b54f0056d67e62467a5a50
data/CHANGELOG.md CHANGED
@@ -1,4 +1,11 @@
1
- ## Unreleased
1
+ ## 3.0.4
2
+
3
+ - Avoid using distinct which may fail in postgresql if the model has json column ([935](https://github.com/scambra/devise_invitable/issues/935))
4
+ - Use scoped preloaded invitations when checking whether an invitation is
5
+ pending, avoiding per-resource queries in listings
6
+ ([#937](https://github.com/scambra/devise_invitable/issues/937))
7
+
8
+ ## 3.0.3
2
9
 
3
10
  - Check invitation token uniqueness against the invitations store instead of
4
11
  the resource table, which no longer has an `invitation_token` column after
@@ -38,7 +38,13 @@ module DeviseInvitable
38
38
  index({ invitable_type: 1, invite_key_digest: 1 })
39
39
  end
40
40
 
41
- belongs_to :invitable, polymorphic: true, optional: true
41
+ invitable_options = { polymorphic: true, optional: true }
42
+ if defined?(Mongoid::Document) && !defined?(ActiveRecord::Base)
43
+ # Mongoid cannot infer a polymorphic inverse when the parent exposes both
44
+ # the complete and scoped invitation associations.
45
+ invitable_options[:inverse_of] = false
46
+ end
47
+ belongs_to :invitable, **invitable_options
42
48
  invited_by_options = { polymorphic: true, optional: true, autosave: true }
43
49
  if defined?(ActiveRecord::Base) && Devise.invited_by_counter_cache
44
50
  invited_by_options[:counter_cache] = Devise.invited_by_counter_cache
@@ -35,11 +35,25 @@ module Devise
35
35
  included do
36
36
  include ::DeviseInvitable::Inviter
37
37
  has_many :devise_invitations,
38
- as: :invitable,
39
- class_name: 'DeviseInvitable::Invitation',
40
- dependent: :nullify,
41
- autosave: true
42
-
38
+ as: :invitable,
39
+ class_name: 'DeviseInvitable::Invitation',
40
+ dependent: :nullify,
41
+ autosave: true
42
+ if defined?(Mongoid::Document) && self < Mongoid::Document
43
+ has_many :pending_invitations,
44
+ as: :invitable,
45
+ class_name: 'DeviseInvitable::Invitation',
46
+ foreign_key: :invitable_id,
47
+ inverse_of: false,
48
+ scope: -> { pending.provisioning }
49
+ else
50
+ has_many :pending_invitations,
51
+ -> { pending.provisioning },
52
+ as: :invitable,
53
+ class_name: 'DeviseInvitable::Invitation',
54
+ foreign_key: :invitable_id,
55
+ inverse_of: false
56
+ end
43
57
  extend ActiveModel::Callbacks
44
58
  define_model_callbacks :invitation_created
45
59
  define_model_callbacks :invitation_accepted
@@ -68,14 +82,22 @@ module Devise
68
82
  }
69
83
  elsif defined?(ActiveRecord) && defined?(ActiveRecord::Base) && self < ActiveRecord::Base
70
84
  scope :created_by_invite, lambda {
71
- joins(:devise_invitations).merge(DeviseInvitable::Invitation.provisioning).distinct
85
+ invitation_ids = DeviseInvitable::Invitation.provisioning
86
+ .where(invitable_type: devise_invitable_base_class.name)
87
+ .select(:invitable_id)
88
+ where(id: invitation_ids)
72
89
  }
73
90
  scope :invitation_not_accepted, lambda {
74
- joins(:devise_invitations).merge(DeviseInvitable::Invitation.pending)
75
- .merge(DeviseInvitable::Invitation.provisioning).distinct
91
+ invitation_ids = DeviseInvitable::Invitation.pending.provisioning
92
+ .where(invitable_type: devise_invitable_base_class.name)
93
+ .select(:invitable_id)
94
+ where(id: invitation_ids)
76
95
  }
77
96
  scope :invitation_accepted, lambda {
78
- joins(:devise_invitations).merge(DeviseInvitable::Invitation.accepted).distinct
97
+ invitation_ids = DeviseInvitable::Invitation.accepted
98
+ .where(invitable_type: devise_invitable_base_class.name)
99
+ .select(:invitable_id)
100
+ where(id: invitation_ids)
79
101
  }
80
102
  scope :no_active_invitation, lambda {
81
103
  invitation_ids = DeviseInvitable::Invitation.pending
@@ -195,6 +217,7 @@ module Devise
195
217
  current_invitation.invitation_token = nil
196
218
  current_invitation.save!
197
219
  supersede_other_invitations!(accepted_at)
220
+ reset_devise_invitation_associations
198
221
  true
199
222
  else
200
223
  false
@@ -222,7 +245,12 @@ module Devise
222
245
  def invitation_pending?
223
246
  return false unless persisted?
224
247
 
225
- devise_invitations.pending.provisioning.exists?
248
+ invitations = pending_invitations
249
+ if devise_invitable_association_loaded?(invitations)
250
+ return invitations.any?
251
+ end
252
+
253
+ invitations.exists?
226
254
  end
227
255
 
228
256
  def invited_to_sign_up?
@@ -276,6 +304,7 @@ module Devise
276
304
  invitation.resource_created_at = now if resource_was_new
277
305
  end
278
306
  invitation.save!
307
+ reset_devise_invitation_associations
279
308
  invitation.invited_by.decrement_invitation_limit! if invitation.invited_by.present?
280
309
  deliver_invitation(options) unless skip_invitation
281
310
  end
@@ -371,24 +400,39 @@ module Devise
371
400
  invitation = current_invitation
372
401
  return unless invitation&.pending?
373
402
 
374
- remove_placeholder = invitation.provisioning? && invitation_pending?
403
+ remove_placeholder = invitation.provisioning?
375
404
  invitation.update!(revoked_at: Time.now.utc, invitation_token: nil)
405
+ reset_devise_invitation_associations
376
406
  destroy if remove_placeholder
377
407
  end
378
408
 
379
409
  protected
380
410
 
411
+ def devise_invitable_association_loaded?(association)
412
+ return association.loaded? if association.respond_to?(:loaded?)
413
+
414
+ association._loaded?
415
+ end
416
+
381
417
  def assign_invitation_attribute(attribute, value)
382
418
  return unless current_invitation
383
419
 
384
420
  current_invitation.public_send("#{attribute}=", value)
385
- current_invitation.save! if current_invitation.persisted?
421
+ if current_invitation.persisted?
422
+ current_invitation.save!
423
+ reset_devise_invitation_associations
424
+ end
386
425
  end
387
426
 
388
427
  def block_from_invitation?
389
428
  invitation_pending?
390
429
  end
391
430
 
431
+ def reset_devise_invitation_associations
432
+ devise_invitations.reset
433
+ pending_invitations.reset
434
+ end
435
+
392
436
  # Checks if the invitation for the user is within the limit time.
393
437
  # We do this by calculating if the difference between today and the
394
438
  # invitation sent date does not exceed the invite for time configured.
@@ -464,6 +508,7 @@ module Devise
464
508
  superseded_reason: 'registered'
465
509
  )
466
510
  end
511
+ reset_devise_invitation_associations
467
512
  self.registering_directly = false
468
513
  end
469
514
 
@@ -1,3 +1,3 @@
1
1
  module DeviseInvitable
2
- VERSION = '3.0.3'.freeze
2
+ VERSION = '3.0.4'.freeze
3
3
  end
@@ -88,7 +88,7 @@ class InvitableTest < ActiveSupport::TestCase
88
88
 
89
89
  test 'should return the correct raw_invitation_token ' do
90
90
  user = new_user
91
- raw, enc = Devise.token_generator.generate(user.class, :invitation_token)
91
+ raw, enc = Devise.token_generator.generate(DeviseInvitable::Invitation, :invitation_token)
92
92
  # stub the generator so the tokens are the same
93
93
  Devise.token_generator.stubs(:generate).returns([raw, enc])
94
94
  user.invite!
@@ -839,6 +839,16 @@ class InvitableTest < ActiveSupport::TestCase
839
839
  assert_equal 3, User.created_by_invite.count
840
840
  end
841
841
 
842
+ test 'invitation scopes do not use distinct with json columns' do
843
+ skip unless defined?(ActiveRecord)
844
+
845
+ assert_equal :json, User.type_for_attribute('options').type
846
+
847
+ [:created_by_invite, :invitation_not_accepted, :invitation_accepted].each do |scope|
848
+ refute_match(/\bSELECT\s+DISTINCT\b/i, User.public_send(scope).to_sql)
849
+ end
850
+ end
851
+
842
852
  test "should preserve return values of Devise::Recoverable#reset_password" do
843
853
  user = new_user
844
854
  retval = user.reset_password('anewpassword', 'anewpassword')
@@ -138,6 +138,7 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
138
138
 
139
139
  user = User.invite!(email: 'placeholder@example.com')
140
140
  user.update!(confirmed_at: Time.now.utc)
141
+ user.pending_invitations.to_a
141
142
 
142
143
  assert_predicate user, :invitation_pending?
143
144
  refute_predicate user, :active_for_authentication?
@@ -149,6 +150,34 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
149
150
  assert_predicate user, :active_for_authentication?
150
151
  end
151
152
 
153
+ test 'invitation pending uses preloaded invitations' do
154
+ pending_user = User.invite!(email: 'pending@example.com')
155
+ active_user = User.create!(email: 'active@example.com', password: '123456789')
156
+ DeviseInvitable::Invitation.create!(
157
+ invitable: active_user,
158
+ invitation_accepted_at: Time.now.utc,
159
+ resource_created_at: Time.now.utc,
160
+ invite_key: { email: active_user.email }
161
+ )
162
+ users = User.includes(:pending_invitations).index_by(&:email)
163
+
164
+ users.each_value do |user|
165
+ assert association_loaded?(user.pending_invitations)
166
+ end
167
+ check_statuses = proc do
168
+ assert_equal 1, users[pending_user.email].pending_invitations.size
169
+ assert_empty users[active_user.email].pending_invitations
170
+ assert_predicate users[pending_user.email], :invitation_pending?
171
+ refute_predicate users[active_user.email], :invitation_pending?
172
+ end
173
+
174
+ if respond_to?(:assert_no_queries)
175
+ assert_no_queries(&check_statuses)
176
+ else
177
+ check_statuses.call
178
+ end
179
+ end
180
+
152
181
  test 'resource attributes raise when placeholder creation is disabled' do
153
182
  User.create_resource_on_invitation = false
154
183
 
@@ -224,4 +253,12 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
224
253
  refute first.reload.attributes.key?('invitation_created_at')
225
254
  refute second.reload.attributes.key?('invitation_created_at')
226
255
  end
256
+
257
+ private
258
+
259
+ def association_loaded?(association)
260
+ return association.loaded? if association.respond_to?(:loaded?)
261
+
262
+ association._loaded?
263
+ end
227
264
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise_invitable
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3
4
+ version: 3.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Cambra