devise_invitable 3.0.1 → 3.0.2

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: 96e5afbd2a5df8b9d94f1b1cbe30a2544f6c759b9c5c4ccb4f1de2a37a8f6e3a
4
- data.tar.gz: ccef0234148611c07c9eae801b104da63066fd856ea3d1bd592180936e1a3042
3
+ metadata.gz: 54bcb44af270be6f5192f01771df589f95045ee16206315e7784c16c49d93df5
4
+ data.tar.gz: 74fea42d1bae1ec385ca62291817679a59c3430fc6f4e609c966ee08db17c2d3
5
5
  SHA512:
6
- metadata.gz: e81ee3d1513dded1dbaaf4685bea5fe71c6d4f08bc57bec248407488ce9f0122498a3dac8c9e80a0db7cf8812bc36c88b570d4688cad52360076d439e172a8ae
7
- data.tar.gz: db4f74177f9f44b361f689a3ebb58e49e42827e07ee39f98dfb9c907a0d1e4e2704381d24e101d41014fc8a1125eb9dbdd01131e694b572580669bb7c8ae2b9d
6
+ metadata.gz: dd0dbba52b99e4117a02072d3f48fbb3623614697a5e972a4f99081351479c9a2846696752a112ffd63d9528631ef196763146af5cc2792996246bcdaa755a97
7
+ data.tar.gz: 406bdf51f7114cc26354cf682658a3f44cb192db2f94cb45d0c7fd72384774d1464678275aac671a973b1660031084c21b4a290279e8e52a9ea69109a8186068
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## Unreleased
2
2
 
3
+ ## 3.0.2
4
+
5
+ - Add `DeviseInvitable::Invitation#deliver` to send a skipped pending
6
+ invitation later, including resource-less invitations
7
+ - Start the `invite_for` validity period when an invitation is sent instead of
8
+ when it is created, so approval delays do not shorten the acceptance window
9
+ - Leave an invitation unsent when delivery fails so it can be retried
10
+
3
11
  ## 3.0.1
4
12
 
5
13
  - Raise `DeviseInvitable::AmbiguousInviteKeyError` instead of selecting an
data/README.md CHANGED
@@ -470,16 +470,32 @@ user and will be decrypted when received.
470
470
  accept_user_invitation_url(invitation_token: user.raw_invitation_token)
471
471
  ```
472
472
 
473
- When `skip_invitation` is used, you must also then set the
474
- `invitation_sent_at` field when the user is sent their token. Failure to do so
475
- will yield "Invalid invitation token" error when the user attempts to accept
476
- the invite. You can set the column, or call `deliver_invitation` to send the
477
- invitation and set the column:
473
+ When `skip_invitation` is used, the resulting invitation remains pending with
474
+ an unset `invitation_sent_at`. Deliver it later by calling `deliver` on the
475
+ invitation:
478
476
 
479
477
  ```ruby
480
- user.deliver_invitation
478
+ invitation = user.current_invitation
479
+ invitation.deliver
481
480
  ```
482
481
 
482
+ This works for both placeholder-backed and resource-less invitations. For a
483
+ resource-less invitation, `deliver` reconstructs the resource from the stored
484
+ invite key. It generates a fresh token, sends the email, and sets
485
+ `invitation_sent_at`. Calling it again after successful delivery does nothing.
486
+
487
+ For example, an approval workflow can find unsent pending invitations and
488
+ deliver an approved one after applying its own authorization policy:
489
+
490
+ ```ruby
491
+ invitations = DeviseInvitable::Invitation.pending.where(invitation_sent_at: nil)
492
+ invitation = invitations.find(approved_invitation_id)
493
+ invitation.deliver
494
+ ```
495
+
496
+ Invitation validity starts at `invitation_sent_at`, so time spent awaiting
497
+ approval does not reduce the configured `invite_for` period.
498
+
483
499
  You can add `:skip_invitation` to attributes hash if `skip_invitation` is
484
500
  added to `attr_accessible`.
485
501
 
@@ -495,12 +511,6 @@ User.invite!(email: 'new_user@example.com', skip_invitation: true)
495
511
  To check if a particular user is created by invitation, irrespective to state
496
512
  of invitation one can use `created_by_invite?`
497
513
 
498
- **Warning**
499
-
500
- When using `skip_invitation` you must send the email with the user object
501
- instance that generated the tokens, as `user.raw_invitation_token` is
502
- available only to the instance and is not persisted in the database.
503
-
504
514
  You can also set `invited_by` when using the `invite!` class method:
505
515
 
506
516
  ```ruby
@@ -67,7 +67,18 @@ module DeviseInvitable
67
67
  end
68
68
 
69
69
  def invitation_period_valid?(period)
70
- period.to_i.zero? || (invitation_created_at && invitation_created_at.utc >= period.ago)
70
+ invitation_sent_at &&
71
+ (period.to_i.zero? || invitation_sent_at.utc >= period.ago)
72
+ end
73
+
74
+ # Deliver a pending invitation that has not been sent yet. Resource-less
75
+ # invitations are reconstructed from their stored invite key.
76
+ def deliver(options = {})
77
+ return unless pending? && invitation_sent_at.nil?
78
+
79
+ resource = invitable || invitable_type.constantize.new(invite_key)
80
+ resource.instance_variable_set(:@current_invitation, self)
81
+ resource.deliver_invitation(options)
71
82
  end
72
83
 
73
84
  def invite_key
@@ -267,7 +267,6 @@ module Devise
267
267
  run_callbacks :invitation_created do
268
268
  now = Time.now.utc
269
269
  invitation.invitation_created_at = now
270
- invitation.invitation_sent_at = now unless skip_invitation
271
270
 
272
271
  validate = options.key?(:validate) ? options[:validate] : self.class.validate_on_invite
273
272
  save_invitation = !self.class.create_resource_on_invitation || save(validate: validate)
@@ -332,10 +331,15 @@ module Devise
332
331
  # Deliver the invitation email
333
332
  def deliver_invitation(options = {})
334
333
  generate_invitation_token! unless @raw_invitation_token
334
+ marked_as_sent = false
335
335
  unless current_invitation.invitation_sent_at
336
336
  current_invitation.update_attribute :invitation_sent_at, Time.now.utc
337
+ marked_as_sent = true
337
338
  end
338
339
  send_devise_notification(:invitation_instructions, @raw_invitation_token, options)
340
+ rescue
341
+ current_invitation.update_attribute(:invitation_sent_at, nil) if marked_as_sent
342
+ raise
339
343
  end
340
344
 
341
345
  # provide alias to the encrypted invitation_token stored by devise
@@ -350,7 +354,7 @@ module Devise
350
354
  def invitation_due_at
351
355
  return nil if (self.class.invite_for == 0 || self.class.invite_for.nil?)
352
356
 
353
- return unless (time = self.invitation_created_at || self.invitation_sent_at)
357
+ return unless (time = self.invitation_sent_at)
354
358
 
355
359
  time + self.class.invite_for
356
360
  end
@@ -1,3 +1,3 @@
1
1
  module DeviseInvitable
2
- VERSION = '3.0.1'.freeze
2
+ VERSION = '3.0.2'.freeze
3
3
  end
@@ -113,27 +113,31 @@ class InvitableTest < ActiveSupport::TestCase
113
113
  user = User.invite!(email: 'valid@email.com')
114
114
 
115
115
  User.stubs(:invite_for).returns(nil)
116
- user.invitation_created_at = Time.now.utc
116
+ user.invitation_sent_at = Time.now.utc
117
117
  assert_predicate user, :valid_invitation?
118
118
 
119
119
  User.stubs(:invite_for).returns(nil)
120
- user.invitation_created_at = 1.year.ago
120
+ user.invitation_sent_at = 1.year.ago
121
121
  assert_predicate user, :valid_invitation?
122
122
 
123
123
  User.stubs(:invite_for).returns(0)
124
- user.invitation_created_at = Time.now.utc
124
+ user.invitation_sent_at = Time.now.utc
125
125
  assert_predicate user, :valid_invitation?
126
126
 
127
127
  User.stubs(:invite_for).returns(0)
128
- user.invitation_created_at = 1.day.ago
128
+ user.invitation_sent_at = 1.day.ago
129
129
  assert_predicate user, :valid_invitation?
130
130
 
131
131
  User.stubs(:invite_for).returns(1.day)
132
- user.invitation_created_at = Time.now.utc
132
+ user.invitation_sent_at = Time.now.utc
133
133
  assert_predicate user, :valid_invitation?
134
134
 
135
135
  User.stubs(:invite_for).returns(1.day)
136
- user.invitation_created_at = 2.days.ago
136
+ user.invitation_sent_at = 2.days.ago
137
+ refute_predicate user, :valid_invitation?
138
+
139
+ User.stubs(:invite_for).returns(0)
140
+ user.invitation_sent_at = nil
137
141
  refute_predicate user, :valid_invitation?
138
142
  end
139
143
 
@@ -141,8 +145,7 @@ class InvitableTest < ActiveSupport::TestCase
141
145
  User.stubs(:invite_for).returns(1.day)
142
146
 
143
147
  user = User.invite!(email: 'valid@email.com')
144
- sent_at = user.invitation_created_at || user.invitation_sent_at
145
- valid_until = sent_at + User.invite_for
148
+ valid_until = user.invitation_sent_at + User.invite_for
146
149
 
147
150
  assert_equal user.invitation_due_at, valid_until
148
151
  end
@@ -294,7 +297,7 @@ class InvitableTest < ActiveSupport::TestCase
294
297
  User.stubs(:invite_for).returns(1.day)
295
298
  user = User.invite!(email: 'valid@email.com')
296
299
  assert user.invited_to_sign_up?
297
- user.invitation_created_at = Time.now.utc - 2.days
300
+ user.invitation_sent_at = Time.now.utc - 2.days
298
301
  token, user.reset_password_token = Devise.token_generator.generate(User, :reset_password_token)
299
302
  user.reset_password_sent_at = Time.now.utc
300
303
  user.save
@@ -539,7 +542,7 @@ class InvitableTest < ActiveSupport::TestCase
539
542
  test 'should return record with errors if invitation_token has expired' do
540
543
  User.stubs(:invite_for).returns(10.hours)
541
544
  invited_user = User.invite!(email: 'valid@email.com')
542
- invited_user.invitation_created_at = 2.days.ago
545
+ invited_user.invitation_sent_at = 2.days.ago
543
546
  invited_user.save(validate: false)
544
547
  user = User.accept_invitation!(invitation_token: Thread.current[:token])
545
548
  assert_equal user, invited_user
@@ -26,6 +26,38 @@ class SeparateInvitationsTest < ActiveSupport::TestCase
26
26
  assert_includes User.no_active_invitation, registered
27
27
  end
28
28
 
29
+ test 'an unsent resource-less invitation can be delivered later' do
30
+ User.create_resource_on_invitation = false
31
+ User.stubs(:invite_for).returns(1.day)
32
+
33
+ invited = User.invite!(email: 'deferred@example.com', skip_invitation: true)
34
+ invitation = invited.current_invitation
35
+ invitation.update!(invitation_created_at: 2.days.ago)
36
+
37
+ assert_nil invitation.invitation_sent_at
38
+ assert_difference('ActionMailer::Base.deliveries.size', 1) do
39
+ invitation.reload.deliver
40
+ end
41
+
42
+ assert_predicate invitation.reload, :invitation_sent_at
43
+ assert invitation.invitation_period_valid?(1.day)
44
+ assert_equal ['deferred@example.com'], ActionMailer::Base.deliveries.last.to
45
+
46
+ assert_no_difference('ActionMailer::Base.deliveries.size') do
47
+ invitation.deliver
48
+ end
49
+ end
50
+
51
+ test 'a failed later delivery remains unsent and can be retried' do
52
+ User.create_resource_on_invitation = false
53
+ invited = User.invite!(email: 'retry@example.com', skip_invitation: true)
54
+ invitation = invited.current_invitation.reload
55
+ User.any_instance.stubs(:send_devise_notification).raises('delivery failed')
56
+
57
+ assert_raises(RuntimeError) { invitation.deliver }
58
+ assert_nil invitation.reload.invitation_sent_at
59
+ end
60
+
29
61
  test 'accepting one of multiple invitations creates the resource and supersedes the others' do
30
62
  User.create_resource_on_invitation = false
31
63
  first_inviter = User.create!(email: 'first@example.com', password: '123456789')
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.1
4
+ version: 3.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Cambra