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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +22 -12
- data/lib/devise_invitable/invitation.rb +12 -1
- data/lib/devise_invitable/models.rb +6 -2
- data/lib/devise_invitable/version.rb +1 -1
- data/test/models/invitable_test.rb +13 -10
- data/test/models/separate_invitations_test.rb +32 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54bcb44af270be6f5192f01771df589f95045ee16206315e7784c16c49d93df5
|
|
4
|
+
data.tar.gz: 74fea42d1bae1ec385ca62291817679a59c3430fc6f4e609c966ee08db17c2d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,
|
|
474
|
-
`invitation_sent_at
|
|
475
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
357
|
+
return unless (time = self.invitation_sent_at)
|
|
354
358
|
|
|
355
359
|
time + self.class.invite_for
|
|
356
360
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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')
|