devise_invitable 1.0.1 → 1.1.0

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.
data/README.rdoc CHANGED
@@ -1,5 +1,5 @@
1
1
  = DeviseInvitable
2
- {<img src="https://secure.travis-ci.org/scambra/devise_invitable.png"/>}[http://travis-ci.org/scambra/devise_invitable]
2
+ {<img src="http://travis-ci.org/scambra/devise_invitable.png"/>}[http://travis-ci.org/scambra/devise_invitable]
3
3
 
4
4
  It adds support to devise[http://github.com/plataformatec/devise] for send invitations by email (it requires to be authenticated) and accept the invitation setting the password.
5
5
 
@@ -90,12 +90,14 @@ or directly as parameters to the <tt>devise</tt> method:
90
90
 
91
91
  * invitation_limit: The number of invitations users can send. The default value of nil means users can send as many invites as they want. A setting of 0 means they can't send invitations. A setting n > 0 means they can send n invitations.
92
92
 
93
- * invite_key: The key to be used to check existing users when sending an invitation. The key must be an unique field. The default value is looking for users by email.
93
+ * invite_key: The key to be used to check existing users when sending an invitation. You can use multiple keys. This value must be a hash with the invite key as hash keys, and regexp to validate format as values. If you don't to validate the key you can set nil as validation format. The default value is looking for users by email and validating with Devise.email_regexp {:email => Devise.email_regexp}.
94
94
 
95
95
  * validate_on_invite: force a record to be valid before being actually invited.
96
96
 
97
97
  * resend_invitation: resend invitation if user with invited status is invited again. Enabled by default.
98
98
 
99
+ * invited_by_class_name: The class name of the inviting model. If this is nil, polymorphic association is used.
100
+
99
101
  For more details, see <tt>config/initializers/devise.rb</tt> (after you invoked the "devise_invitable:install" generator described above).
100
102
 
101
103
  == Configuring views
@@ -153,6 +155,15 @@ You can add :skip_invitation to attributes hash if skip_invitation is added to a
153
155
  User.invite!(:email => "new_user@example.com", :name => "John Doe", :skip_invitation => true)
154
156
  # => the record will be created, but the invitation email will not be sent
155
157
 
158
+ You can send an invitation to an existing user if your workflow creates them separately:
159
+
160
+ user = User.find(42)
161
+ user.invite!(current_user) # current user is optional to set the invited_by attribute
162
+
163
+ You can also set <tt>invited_by</tt> when using the <tt>invite!</tt> class method:
164
+
165
+ User.invite!({:email => "new_user@example.com"}, current_user) # current_user will be set as invited_by
166
+
156
167
  === Accept an invitation
157
168
 
158
169
  To accept an invitation with a token use the <tt>accept_invitation!</tt> class method. <tt>:invitation_token</tt> must be present in the parameters hash. You can also include other attributes in the hash.
@@ -171,6 +182,13 @@ A callback event is fired before and after an invitation is accepted (User#accep
171
182
 
172
183
  The callbacks support all options and arguments available to the standard callbacks provided by AR.
173
184
 
185
+ === Scopes
186
+
187
+ A pair of scopes to find those users that have accepted, and those that have not accepted, invitations are defined:
188
+
189
+ User.invitation_accepted # => returns all Users for whom the invitation_accepted_at attribute is not nil
190
+ User.invitation_not_accepted # => returns all Users for whom the invitation_accepted_at attribute is nil
191
+
174
192
  == Integration in a Rails application
175
193
 
176
194
  Since the invitations controller take care of all the creation/acceptation of an invitation, in most cases you wouldn't call the <tt>invite!</tt> and <tt>accept_invitation!</tt> methods directly.
@@ -272,4 +290,4 @@ Special thanks to rymai[http://github.com/rymai] for the Rails 3 support, his fo
272
290
 
273
291
  == Copyright
274
292
 
275
- Copyright (c) 2009 Sergio Cambra. See LICENSE for details.
293
+ Copyright (c) 2012 Sergio Cambra. See LICENSE for details.
@@ -13,7 +13,7 @@ class Devise::InvitationsController < DeviseController
13
13
 
14
14
  # POST /resource/invitation
15
15
  def create
16
- self.resource = resource_class.invite!(params[resource_name], current_inviter)
16
+ self.resource = resource_class.invite!(resource_params, current_inviter)
17
17
 
18
18
  if resource.errors.empty?
19
19
  set_flash_message :notice, :send_instructions, :email => self.resource.email
@@ -35,7 +35,7 @@ class Devise::InvitationsController < DeviseController
35
35
 
36
36
  # PUT /resource/invitation
37
37
  def update
38
- self.resource = resource_class.accept_invitation!(params[resource_name])
38
+ self.resource = resource_class.accept_invitation!(resource_params)
39
39
 
40
40
  if resource.errors.empty?
41
41
  set_flash_message :notice, :updated
@@ -1,4 +1,4 @@
1
- <h2>Set your password</h2>
1
+ <h2><%= t 'devise.invitations.edit.header' %></h2>
2
2
 
3
3
  <%= form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => { :method => :put } do |f| %>
4
4
  <%= devise_error_messages! %>
@@ -10,5 +10,5 @@
10
10
  <p><%= f.label :password_confirmation %><br />
11
11
  <%= f.password_field :password_confirmation %></p>
12
12
 
13
- <p><%= f.submit "Set my password" %></p>
13
+ <p><%= f.submit t("devise.invitations.edit.submit_button") %></p>
14
14
  <% end %>
@@ -1,12 +1,12 @@
1
- <h2>Send invitation</h2>
1
+ <h2><%= t "devise.invitations.new.header" %></h2>
2
2
 
3
3
  <%= form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => {:method => :post} do |f| %>
4
4
  <%= devise_error_messages! %>
5
5
 
6
- <p><%= f.label :email %><br />
7
- <%= f.text_field :email %></p>
6
+ <% resource.class.invite_key_fields.each do |field| -%>
7
+ <p><%= f.label field %><br />
8
+ <%= f.text_field field %></p>
9
+ <% end -%>
8
10
 
9
- <p><%= f.submit "Send an invitation" %></p>
11
+ <p><%= f.submit t("devise.invitations.new.submit_button") %></p>
10
12
  <% end %>
11
-
12
- <%= link_to "Home", after_sign_in_path_for(resource_name) %><br />
@@ -5,6 +5,12 @@ en:
5
5
  invitation_token_invalid: 'The invitation token provided is not valid!'
6
6
  updated: 'Your password was set successfully. You are now signed in.'
7
7
  no_invitations_remaining: "No invitations remaining"
8
+ new:
9
+ header: "Send invitation"
10
+ submit_button: "Send an invitation"
11
+ edit:
12
+ header: "Set your password"
13
+ submit_button: "Set my password"
8
14
  mailer:
9
15
  invitation_instructions:
10
16
  subject: 'Invitation instructions'
@@ -1,36 +1,39 @@
1
1
  module DeviseInvitable::Controllers::Registrations
2
2
  def self.included(controller)
3
- controller.send :around_filter, :destroy_if_previously_invited, :only => :create
3
+ controller.send :around_filter, :keep_invitation_info, :only => :create
4
4
  end
5
5
 
6
6
  protected
7
7
 
8
8
  def destroy_if_previously_invited
9
- invitation_info = {}
10
-
11
9
  hash = params[resource_name]
12
10
  if hash && hash[:email]
13
- resource = resource_class.first(:conditions => { :email => hash[:email], :encrypted_password => '' })
11
+ resource = resource_class.where(:email => hash[:email], :encrypted_password => '').first
14
12
  if resource
15
- invitation_info[:invitation_sent_at] = resource[:invitation_sent_at]
16
- invitation_info[:invited_by_id] = resource[:invited_by_id]
17
- invitation_info[:invited_by_type] = resource[:invited_by_type]
13
+ @invitation_info = Hash[resource.invitation_fields.map {|field|
14
+ [field, resource[field]]
15
+ }]
18
16
  resource.destroy
19
17
  end
20
18
  end
19
+ end
21
20
 
22
- # execute the action (create)
21
+ def keep_invitation_info
22
+ resource_invitable = resource_class.devise_modules.include?(:invitable)
23
+ destroy_if_previously_invited if resource_invitable
23
24
  yield
24
- # Note that the after_filter is executed at THIS position !
25
+ reset_invitation_info if resource_invitable
26
+ end
25
27
 
28
+ def reset_invitation_info
26
29
  # Restore info about the last invitation (for later reference)
27
30
  # Reset the invitation_info only, if invited_by_id is still nil at this stage:
28
- resource = resource_class.first(:conditions => { :email => hash[:email], :invited_by_id => nil })
29
- if resource
30
- resource[:invitation_sent_at] = invitation_info[:invitation_sent_at]
31
- resource[:invited_by_id] = invitation_info[:invited_by_id]
32
- resource[:invited_by_type] = invitation_info[:invited_by_type]
31
+ resource = resource_class.where(:email => params[resource_name][:email], :invited_by_id => nil).first
32
+ if resource && @invitation_info
33
+ resource.invitation_fields.each do |field|
34
+ resource[field] = @invitation_info[field]
35
+ end
33
36
  resource.save!
34
37
  end
35
38
  end
36
- end
39
+ end
@@ -0,0 +1,36 @@
1
+ module DeviseInvitable::Controllers::Registrations
2
+ def self.included(controller)
3
+ controller.send :around_filter, :destroy_if_previously_invited, :only => :create
4
+ end
5
+
6
+ protected
7
+
8
+ def destroy_if_previously_invited
9
+ invitation_info = {}
10
+
11
+ hash = params[resource_name]
12
+ if resource_class.modules.include?(:invitable) && hash && hash[:email]
13
+ resource = resource_class.where(:email => hash[:email], :encrypted_password => '').first
14
+ if resource
15
+ invitation_info[:invitation_sent_at] = resource[:invitation_sent_at]
16
+ invitation_info[:invited_by_id] = resource[:invited_by_id]
17
+ invitation_info[:invited_by_type] = resource[:invited_by_type]
18
+ resource.destroy
19
+ end
20
+ end
21
+
22
+ # execute the action (create)
23
+ yield
24
+ # Note that the after_filter is executed at THIS position !
25
+
26
+ # Restore info about the last invitation (for later reference)
27
+ # Reset the invitation_info only, if invited_by_id is still nil at this stage:
28
+ resource = resource_class.where(:email => hash[:email], :invited_by_id => nil).first
29
+ if resource
30
+ resource[:invitation_sent_at] = invitation_info[:invitation_sent_at]
31
+ resource[:invited_by_id] = invitation_info[:invited_by_id]
32
+ resource[:invited_by_type] = invitation_info[:invited_by_type]
33
+ resource.save!
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,6 @@
1
1
  module DeviseInvitable
2
2
  module Mailer
3
-
3
+
4
4
  # Deliver an invitation email
5
5
  def invitation_instructions(record)
6
6
  devise_mail(record, :invitation_instructions)
@@ -1,3 +1,5 @@
1
+ require 'active_support/deprecation'
2
+
1
3
  module Devise
2
4
  module Models
3
5
  # Invitable is responsible for sending invitation emails.
@@ -13,7 +15,7 @@ module Devise
13
15
  #
14
16
  # Examples:
15
17
  #
16
- # User.find(1).invited? # => true/false
18
+ # User.find(1).invited_to_sign_up? # => true/false
17
19
  # User.invite!(:email => 'someone@example.com') # => send invitation
18
20
  # User.accept_invitation!(:invitation_token => '...') # => accept invitation with a token
19
21
  # User.find(1).accept_invitation! # => accept invitation
@@ -22,45 +24,102 @@ module Devise
22
24
  extend ActiveSupport::Concern
23
25
 
24
26
  attr_accessor :skip_invitation
27
+ attr_accessor :completing_invite
25
28
 
26
29
  included do
27
- include ::DeviseInvitable::Inviter
28
- belongs_to :invited_by, :polymorphic => true
29
-
30
+ include ::DeviseInvitable::Inviter
31
+ if Devise.invited_by_class_name
32
+ belongs_to :invited_by, :class_name => Devise.invited_by_class_name
33
+ else
34
+ belongs_to :invited_by, :polymorphic => true
35
+ end
36
+
30
37
  include ActiveSupport::Callbacks
31
38
  define_callbacks :invitation_accepted
32
-
39
+
33
40
  attr_writer :skip_password
41
+
42
+ scope :invitation_not_accepted, where(:invitation_accepted_at => nil)
43
+ if defined?(Mongoid) && self < Mongoid::Document
44
+ scope :invitation_accepted, where(:invitation_accepted_at.ne => nil)
45
+ else
46
+ scope :invitation_accepted, where(arel_table[:invitation_accepted_at].not_eq(nil))
47
+
48
+ [:before_invitation_accepted, :after_invitation_accepted].each do |callback_method|
49
+ send callback_method do
50
+ notify_observers callback_method
51
+ end
52
+ end
53
+ end
54
+ end
55
+
56
+ def self.required_fields(klass)
57
+ fields = [:invitation_token, :invitation_sent_at, :invitation_accepted_at,
58
+ :invitation_limit, :invited_by_id, :invited_by_type]
59
+ if Devise.invited_by_class_name
60
+ fields -= :invited_by_type
61
+ end
62
+ fields
63
+ end
64
+
65
+ def invitation_fields
66
+ fields = [:invitation_sent_at, :invited_by_id, :invited_by_type]
67
+ if Devise.invited_by_class_name
68
+ fields -= :invited_by_type
69
+ end
70
+ fields
34
71
  end
35
72
 
36
73
  # Accept an invitation by clearing invitation token and and setting invitation_accepted_at
37
74
  # Confirms it if model is confirmable
38
75
  def accept_invitation!
39
- if self.invited? && self.valid?
76
+ self.invitation_accepted_at = Time.now.utc
77
+ if self.invited_to_sign_up? && self.valid?
40
78
  run_callbacks :invitation_accepted do
41
79
  self.invitation_token = nil
42
- self.invitation_accepted_at = Time.now.utc if respond_to? :"invitation_accepted_at="
43
80
  self.save(:validate => false)
44
81
  end
45
82
  end
46
83
  end
47
84
 
85
+ # Verifies whether a user has accepted an invitation (or is accepting it), or was never invited
86
+ def accepting_or_not_invited?
87
+ ActiveSupport::Deprecation.warn "accepting_or_not_invited? is deprecated and will be removed from DeviseInvitable 1.1.0 (use accepted_or_not_invited? instead)"
88
+ accepted_or_not_invited?
89
+ end
90
+
48
91
  # Verifies whether a user has been invited or not
49
- def invited?
92
+ def invited_to_sign_up?
50
93
  persisted? && invitation_token.present?
51
94
  end
52
95
 
96
+ # Verifies whether a user accepted an invitation (or is accepting it)
97
+ def invitation_accepted?
98
+ invitation_accepted_at.present?
99
+ end
100
+
101
+ # Verifies whether a user has accepted an invitation (or is accepting it), or was never invited
102
+ def accepted_or_not_invited?
103
+ invitation_accepted? || !invited_to_sign_up?
104
+ end
105
+
106
+ def invited?
107
+ ActiveSupport::Deprecation.warn "invited? is deprecated and will be removed from DeviseInvitable 1.1.0 (use invited_to_sign_up? instead)"
108
+ invited_to_sign_up?
109
+ end
110
+
53
111
  # Reset invitation token and send invitation again
54
- def invite!
55
- was_invited = invited?
112
+ def invite!(invited_by = nil)
113
+ was_invited = invited_to_sign_up?
56
114
  self.skip_confirmation! if self.new_record? && self.respond_to?(:skip_confirmation!)
57
115
  generate_invitation_token if self.invitation_token.nil?
58
116
  self.invitation_sent_at = Time.now.utc
59
-
117
+ self.invited_by = invited_by if invited_by
118
+
60
119
  # Call these before_validate methods since we aren't validating on save
61
120
  self.downcase_keys if self.new_record? && self.respond_to?(:downcase_keys)
62
121
  self.strip_whitespace if self.new_record? && self.respond_to?(:strip_whitespace)
63
-
122
+
64
123
  if save(:validate => false)
65
124
  self.invited_by.decrement_invitation_limit! if !was_invited and self.invited_by.present?
66
125
  deliver_invitation unless @skip_invitation
@@ -71,19 +130,26 @@ module Devise
71
130
  # invited, we need to calculate if the invitation time has not expired
72
131
  # for this user, in other words, if the invitation is still valid.
73
132
  def valid_invitation?
74
- invited? && invitation_period_valid?
133
+ invited_to_sign_up? && invitation_period_valid?
75
134
  end
76
135
 
77
136
  # Only verify password when is not invited
78
137
  def valid_password?(password)
79
- super unless invited?
138
+ super unless invited_to_sign_up?
80
139
  end
81
-
140
+
82
141
  def reset_password!(new_password, new_password_confirmation)
83
142
  super
84
143
  accept_invitation!
85
144
  end
86
145
 
146
+ def invite_key_valid?
147
+ return true unless self.class.invite_key.is_a? Hash # FIXME: remove this line when deprecation is removed
148
+ self.class.invite_key.all? do |key, regexp|
149
+ regexp.nil? || self.send(key).try(:match, regexp)
150
+ end
151
+ end
152
+
87
153
  protected
88
154
  # Overriding the method in Devise's :validatable module so password is not required on inviting
89
155
  def password_required?
@@ -92,7 +158,7 @@ module Devise
92
158
 
93
159
  # Deliver the invitation email
94
160
  def deliver_invitation
95
- ::Devise.mailer.invitation_instructions(self).deliver
161
+ send_devise_notification(:invitation_instructions)
96
162
  end
97
163
 
98
164
  # Checks if the invitation for the user is within the limit time.
@@ -125,6 +191,16 @@ module Devise
125
191
  end
126
192
 
127
193
  module ClassMethods
194
+ # Return fields to invite
195
+ def invite_key_fields
196
+ if invite_key.is_a? Hash
197
+ invite_key.keys
198
+ else
199
+ ActiveSupport::Deprecation.warn("invite_key should be a hash like {#{invite_key.inspect} => /.../}")
200
+ Array(invite_key)
201
+ end
202
+ end
203
+
128
204
  # Attempt to find a user by it's email. If a record is not found, create a new
129
205
  # user and send invitation to it. If user is found, returns the user with an
130
206
  # email already exists error.
@@ -132,16 +208,24 @@ module Devise
132
208
  # resend_invitation is set to false
133
209
  # Attributes must contain the user email, other attributes will be set in the record
134
210
  def _invite(attributes={}, invited_by=nil, &block)
135
- invitable = find_or_initialize_with_error_by(invite_key, attributes.delete(invite_key))
211
+ invite_key_array = invite_key_fields
212
+ attributes_hash = {}
213
+ invite_key_array.each do |k,v|
214
+ attributes_hash[k] = attributes.delete(k)
215
+ end
216
+
217
+ invitable = find_or_initialize_with_errors(invite_key_array, attributes_hash)
136
218
  invitable.assign_attributes(attributes, :as => inviter_role(invited_by))
137
219
  invitable.invited_by = invited_by
138
220
 
139
221
  invitable.skip_password = true
140
222
  invitable.valid? if self.validate_on_invite
141
223
  if invitable.new_record?
142
- invitable.errors.clear if !self.validate_on_invite and invitable.email.try(:match, Devise.email_regexp)
143
- else
144
- invitable.errors.add(invite_key, :taken) unless invitable.invited? && self.resend_invitation
224
+ invitable.errors.clear if !self.validate_on_invite and invitable.invite_key_valid?
225
+ elsif !invitable.invited_to_sign_up? || !self.resend_invitation
226
+ invite_key_array.each do |key|
227
+ invitable.errors.add(key, :taken)
228
+ end
145
229
  end
146
230
 
147
231
  if invitable.errors.empty?
@@ -150,7 +234,7 @@ module Devise
150
234
  end
151
235
  [invitable, mail]
152
236
  end
153
-
237
+
154
238
  # Override this method if the invitable is using Mass Assignment Security
155
239
  # and the inviter has a non-default role.
156
240
  def inviter_role(inviter)
@@ -186,16 +270,16 @@ module Devise
186
270
  def invitation_token
187
271
  generate_token(:invitation_token)
188
272
  end
189
-
273
+
190
274
  # Callback convenience methods
191
275
  def before_invitation_accepted(*args, &blk)
192
276
  set_callback(:invitation_accepted, :before, *args, &blk)
193
277
  end
194
-
278
+
195
279
  def after_invitation_accepted(*args, &blk)
196
280
  set_callback(:invitation_accepted, :after, *args, &blk)
197
281
  end
198
-
282
+
199
283
 
200
284
  Devise::Models.config(self, :invite_for)
201
285
  Devise::Models.config(self, :validate_on_invite)
@@ -0,0 +1,224 @@
1
+ require 'active_support/deprecation'
2
+
3
+ module Devise
4
+ module Models
5
+ # Invitable is responsible for sending invitation emails.
6
+ # When an invitation is sent to an email address, an account is created for it.
7
+ # Invitation email contains a link allowing the user to accept the invitation
8
+ # by setting a password (as reset password from Devise's recoverable module).
9
+ #
10
+ # Configuration:
11
+ #
12
+ # invite_for: The period the generated invitation token is valid, after
13
+ # this period, the invited resource won't be able to accept the invitation.
14
+ # When invite_for is 0 (the default), the invitation won't expire.
15
+ #
16
+ # Examples:
17
+ #
18
+ # User.find(1).invited_to_sign_up? # => true/false
19
+ # User.invite!(:email => 'someone@example.com') # => send invitation
20
+ # User.accept_invitation!(:invitation_token => '...') # => accept invitation with a token
21
+ # User.find(1).accept_invitation! # => accept invitation
22
+ # User.find(1).invite! # => reset invitation status and send invitation again
23
+ module Invitable
24
+ extend ActiveSupport::Concern
25
+
26
+ attr_accessor :skip_invitation
27
+ attr_accessor :completing_invite
28
+
29
+ included do
30
+ include ::DeviseInvitable::Inviter
31
+ belongs_to :invited_by, :polymorphic => true
32
+
33
+ include ActiveSupport::Callbacks
34
+ define_callbacks :invitation_accepted
35
+
36
+ attr_writer :skip_password
37
+ end
38
+
39
+ # Accept an invitation by clearing invitation token and and setting invitation_accepted_at
40
+ # Confirms it if model is confirmable
41
+ def accept_invitation!
42
+ self.completing_invite = true
43
+ if self.invited_to_sign_up? && self.valid?
44
+ run_callbacks :invitation_accepted do
45
+ self.invitation_token = nil
46
+ self.invitation_accepted_at = Time.now.utc if respond_to? :"invitation_accepted_at="
47
+ self.completing_invite = false
48
+ self.save(:validate => false)
49
+ end
50
+ end
51
+ end
52
+
53
+ # Verifies whether a user has accepted an invite, was never invited, or is in the process of accepting an invitation, or not
54
+ def accepting_or_not_invited?
55
+ !!completing_invite || invited_to_sign_up?
56
+ end
57
+
58
+ # Verifies whether a user has been invited or not
59
+ def invited_to_sign_up?
60
+ persisted? && invitation_token.present?
61
+ end
62
+
63
+ def invited?
64
+ invited_to_sign_up?
65
+ end
66
+ deprecate :invited?
67
+
68
+ # Reset invitation token and send invitation again
69
+ def invite!
70
+ was_invited = invited_to_sign_up?
71
+ self.skip_confirmation! if self.new_record? && self.respond_to?(:skip_confirmation!)
72
+ generate_invitation_token if self.invitation_token.nil?
73
+ self.invitation_sent_at = Time.now.utc
74
+
75
+ # Call these before_validate methods since we aren't validating on save
76
+ self.downcase_keys if self.new_record? && self.respond_to?(:downcase_keys)
77
+ self.strip_whitespace if self.new_record? && self.respond_to?(:strip_whitespace)
78
+
79
+ if save(:validate => false)
80
+ self.invited_by.decrement_invitation_limit! if !was_invited and self.invited_by.present?
81
+ deliver_invitation unless @skip_invitation
82
+ end
83
+ end
84
+
85
+ # Verify whether a invitation is active or not. If the user has been
86
+ # invited, we need to calculate if the invitation time has not expired
87
+ # for this user, in other words, if the invitation is still valid.
88
+ def valid_invitation?
89
+ invited_to_sign_up? && invitation_period_valid?
90
+ end
91
+
92
+ # Only verify password when is not invited
93
+ def valid_password?(password)
94
+ super unless invited_to_sign_up?
95
+ end
96
+
97
+ def reset_password!(new_password, new_password_confirmation)
98
+ super
99
+ accept_invitation!
100
+ end
101
+
102
+ protected
103
+ # Overriding the method in Devise's :validatable module so password is not required on inviting
104
+ def password_required?
105
+ !@skip_password && super
106
+ end
107
+
108
+ # Deliver the invitation email
109
+ def deliver_invitation
110
+ ::Devise.mailer.invitation_instructions(self).deliver
111
+ end
112
+
113
+ # Checks if the invitation for the user is within the limit time.
114
+ # We do this by calculating if the difference between today and the
115
+ # invitation sent date does not exceed the invite for time configured.
116
+ # Invite_for is a model configuration, must always be an integer value.
117
+ #
118
+ # Example:
119
+ #
120
+ # # invite_for = 1.day and invitation_sent_at = today
121
+ # invitation_period_valid? # returns true
122
+ #
123
+ # # invite_for = 5.days and invitation_sent_at = 4.days.ago
124
+ # invitation_period_valid? # returns true
125
+ #
126
+ # # invite_for = 5.days and invitation_sent_at = 5.days.ago
127
+ # invitation_period_valid? # returns false
128
+ #
129
+ # # invite_for = nil
130
+ # invitation_period_valid? # will always return true
131
+ #
132
+ def invitation_period_valid?
133
+ invitation_sent_at && (self.class.invite_for.to_i.zero? || invitation_sent_at.utc >= self.class.invite_for.ago)
134
+ end
135
+
136
+ # Generates a new random token for invitation, and stores the time
137
+ # this token is being generated
138
+ def generate_invitation_token
139
+ self.invitation_token = self.class.invitation_token
140
+ end
141
+
142
+ module ClassMethods
143
+ # Attempt to find a user by it's email. If a record is not found, create a new
144
+ # user and send invitation to it. If user is found, returns the user with an
145
+ # email already exists error.
146
+ # If user is found and still have pending invitation, email is resend unless
147
+ # resend_invitation is set to false
148
+ # Attributes must contain the user email, other attributes will be set in the record
149
+ def _invite(attributes={}, invited_by=nil, &block)
150
+ invitable = find_or_initialize_with_error_by(invite_key, attributes.delete(invite_key))
151
+ invitable.assign_attributes(attributes, :as => inviter_role(invited_by))
152
+ invitable.invited_by = invited_by
153
+
154
+ invitable.skip_password = true
155
+ invitable.valid? if self.validate_on_invite
156
+ if invitable.new_record?
157
+ invitable.errors.clear if !self.validate_on_invite and invitable.email.try(:match, Devise.email_regexp)
158
+ else
159
+ invitable.errors.add(invite_key, :taken) unless invitable.invited_to_sign_up? && self.resend_invitation
160
+ end
161
+
162
+ if invitable.errors.empty?
163
+ yield invitable if block_given?
164
+ mail = invitable.invite!
165
+ end
166
+ [invitable, mail]
167
+ end
168
+
169
+ # Override this method if the invitable is using Mass Assignment Security
170
+ # and the inviter has a non-default role.
171
+ def inviter_role(inviter)
172
+ :default
173
+ end
174
+
175
+ def invite!(attributes={}, invited_by=nil, &block)
176
+ invitable, mail = _invite(attributes, invited_by, &block)
177
+ invitable
178
+ end
179
+
180
+ def invite_mail!(attributes={}, invited_by=nil, &block)
181
+ invitable, mail = _invite(attributes, invited_by, &block)
182
+ mail
183
+ end
184
+
185
+ # Attempt to find a user by it's invitation_token to set it's password.
186
+ # If a user is found, reset it's password and automatically try saving
187
+ # the record. If not user is found, returns a new user containing an
188
+ # error in invitation_token attribute.
189
+ # Attributes must contain invitation_token, password and confirmation
190
+ def accept_invitation!(attributes={})
191
+ invitable = find_or_initialize_with_error_by(:invitation_token, attributes.delete(:invitation_token))
192
+ invitable.errors.add(:invitation_token, :invalid) if invitable.invitation_token && invitable.persisted? && !invitable.valid_invitation?
193
+ if invitable.errors.empty?
194
+ invitable.attributes = attributes
195
+ invitable.accept_invitation!
196
+ end
197
+ invitable
198
+ end
199
+
200
+ # Generate a token checking if one does not already exist in the database.
201
+ def invitation_token
202
+ generate_token(:invitation_token)
203
+ end
204
+
205
+ # Callback convenience methods
206
+ def before_invitation_accepted(*args, &blk)
207
+ set_callback(:invitation_accepted, :before, *args, &blk)
208
+ end
209
+
210
+ def after_invitation_accepted(*args, &blk)
211
+ set_callback(:invitation_accepted, :after, *args, &blk)
212
+ end
213
+
214
+
215
+ Devise::Models.config(self, :invite_for)
216
+ Devise::Models.config(self, :validate_on_invite)
217
+ Devise::Models.config(self, :invitation_limit)
218
+ Devise::Models.config(self, :invite_key)
219
+ Devise::Models.config(self, :resend_invitation)
220
+ end
221
+ end
222
+ end
223
+ end
224
+
@@ -1,6 +1,6 @@
1
1
  module ActionDispatch::Routing
2
2
  class Mapper
3
-
3
+
4
4
  protected
5
5
  def devise_invitation(mapping, controllers)
6
6
  resource :invitation, :only => [:new, :create, :update],
@@ -8,6 +8,6 @@ module ActionDispatch::Routing
8
8
  get :edit, :path => mapping.path_names[:accept], :as => :accept
9
9
  end
10
10
  end
11
-
11
+
12
12
  end
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module DeviseInvitable
2
- VERSION = '1.0.1'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -37,13 +37,14 @@ module Devise
37
37
  mattr_accessor :invitation_limit
38
38
  @@invitation_limit = nil
39
39
 
40
- # Public: The key to be used to check existing users when sending an invitation
40
+ # Public: The key to be used to check existing users when sending an invitation,
41
+ # and the regexp used to test it when validate_on_invite is not set.
41
42
  #
42
43
  # Examples (in config/initializers/devise.rb)
43
44
  #
44
- # config.invite_key = :email
45
+ # config.invite_key = {:email => /\A[^@]+@[^@]+\z/}
45
46
  mattr_accessor :invite_key
46
- @@invite_key = :email
47
+ @@invite_key = {:email => Devise.email_regexp}
47
48
 
48
49
  # Public: Resend invitation if user with invited status is invited again
49
50
  # (default: true)
@@ -53,6 +54,11 @@ module Devise
53
54
  # config.resend_invitation = false
54
55
  mattr_accessor :resend_invitation
55
56
  @@resend_invitation = true
57
+
58
+ # Public: The class name of the inviting model. If this is nil,
59
+ # the #invited_by association is declared to be polymorphic. (default: nil)
60
+ mattr_accessor :invited_by_class_name
61
+ @@invited_by_class_name = nil
56
62
  end
57
63
 
58
64
  Devise.add_module :invitable, :controller => :invitations, :model => 'devise_invitable/model', :route => :invitation
@@ -3,16 +3,16 @@ module DeviseInvitable
3
3
  class InstallGenerator < Rails::Generators::Base
4
4
  source_root File.expand_path("../../templates", __FILE__)
5
5
  desc "Add DeviseInvitable config variables to the Devise initializer and copy DeviseInvitable locale files to your application."
6
-
6
+
7
7
  # def devise_install
8
8
  # invoke "devise:install"
9
9
  # end
10
-
10
+
11
11
  def add_config_options_to_initializer
12
12
  devise_initializer_path = "config/initializers/devise.rb"
13
13
  if File.exist?(devise_initializer_path)
14
14
  old_content = File.read(devise_initializer_path)
15
-
15
+
16
16
  if old_content.match(Regexp.new(/^\s# ==> Configuration for :invitable\n/))
17
17
  false
18
18
  else
@@ -23,18 +23,20 @@ module DeviseInvitable
23
23
  # this period, the invited resource won't be able to accept the invitation.
24
24
  # When invite_for is 0 (the default), the invitation won't expire.
25
25
  # config.invite_for = 2.weeks
26
-
26
+
27
27
  # Number of invitations users can send.
28
28
  # If invitation_limit is nil, users can send unlimited invitations.
29
29
  # If invitation_limit is 0, users can't send invitations.
30
30
  # If invitation_limit n > 0, users can send n invitations.
31
31
  # Default: nil
32
32
  # config.invitation_limit = 5
33
-
33
+
34
34
  # The key to be used to check existing users when sending an invitation
35
- # config.invite_key = :email
36
-
37
- # Flag that force a record to be valid before being actually invited
35
+ # and the regexp used to test it when validate_on_invite is not set.
36
+ # config.invite_key = {:email => /\A[^@]+@[^@]+\z/}
37
+ # config.invite_key = {:email => /\A[^@]+@[^@]+\z/, :username => nil}
38
+
39
+ # Flag that force a record to be valid before being actually invited
38
40
  # Default: false
39
41
  # config.validate_on_invite = true
40
42
 
@@ -43,11 +45,11 @@ CONTENT
43
45
  end
44
46
  end
45
47
  end
46
-
48
+
47
49
  def copy_locale
48
50
  copy_file "../../../config/locales/en.yml", "config/locales/devise_invitable.en.yml"
49
51
  end
50
-
52
+
51
53
  end
52
54
  end
53
55
  end
@@ -0,0 +1,11 @@
1
+ <h2><%= t 'devise.invitations.edit.header' %></h2>
2
+
3
+ <%= simple_form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => { :method => :put } do |f| %>
4
+ <%= devise_error_messages! %>
5
+ <%= f.hidden_field :invitation_token %>
6
+
7
+ <%= f.input :password %>
8
+ <%= f.input :password_confirmation %>
9
+
10
+ <%= f.submit t("devise.invitations.edit.submit_button") %>
11
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <h2><%= t "devise.invitations.new.header" %></h2>
2
+
3
+ <%= simple_form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => {:method => :post} do |f| %>
4
+ <%= devise_error_messages! %>
5
+
6
+ <% resource.class.invite_key_fields.each do |field| -%>
7
+ <%= f.input field %>
8
+ <% end -%>
9
+
10
+ <%= f.submit t("devise.invitations.new.submit_button") %>
11
+ <% end %>
@@ -2,18 +2,40 @@ require 'generators/devise/views_generator'
2
2
 
3
3
  module DeviseInvitable
4
4
  module Generators
5
- class ViewsGenerator < Rails::Generators::Base
6
- desc 'Copies all DeviseInvitable views to your application.'
5
+ class InvitationViewsGenerator < Rails::Generators::Base
6
+ include ::Devise::Generators::ViewPathTemplates
7
+
8
+ def copy_views
9
+ view_directory :invitations
10
+ end
11
+ end
12
+
13
+ class SimpleFormForGenerator < InvitationViewsGenerator
14
+ source_root File.expand_path("../templates/simple_form_for", __FILE__)
15
+ end
7
16
 
8
- argument :scope, :required => false, :default => nil,
9
- :desc => "The scope to copy views to"
17
+ class FormForGenerator < InvitationViewsGenerator
18
+ source_root File.expand_path("../../../../app/views/devise", __FILE__)
19
+ end
10
20
 
21
+ class MailerViewsGenerator < Rails::Generators::Base
11
22
  include ::Devise::Generators::ViewPathTemplates
12
23
  source_root File.expand_path("../../../../app/views/devise", __FILE__)
24
+ desc "Copies Devise mail erb views to your application."
25
+ hide!
26
+
13
27
  def copy_views
14
- view_directory :invitations
15
28
  view_directory :mailer
16
29
  end
17
30
  end
31
+
32
+ class ViewsGenerator < Rails::Generators::Base
33
+ desc 'Copies all DeviseInvitable views to your application.'
34
+ argument :scope, :required => false, :default => nil, :desc => "The scope to copy views to"
35
+
36
+ invoke MailerViewsGenerator
37
+
38
+ hook_for :form_builder, :aliases => "-b", :desc => "Form builder to be used", :default => defined?(SimpleForm) ? "simple_form_for" : "form_for"
39
+ end
18
40
  end
19
41
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise_invitable
3
3
  version: !ruby/object:Gem::Version
4
- hash: 21
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
- - 0
9
8
  - 1
10
- version: 1.0.1
9
+ - 0
10
+ version: 1.1.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Sergio Cambra
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2012-04-03 00:00:00 Z
18
+ date: 2012-08-20 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: bundler
@@ -23,7 +23,7 @@ dependencies:
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
24
  none: false
25
25
  requirements:
26
- - - ~>
26
+ - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  hash: 19
29
29
  segments:
@@ -34,7 +34,7 @@ dependencies:
34
34
  type: :development
35
35
  version_requirements: *id001
36
36
  - !ruby/object:Gem::Dependency
37
- name: rails
37
+ name: railties
38
38
  prerelease: false
39
39
  requirement: &id002 !ruby/object:Gem::Requirement
40
40
  none: false
@@ -49,21 +49,36 @@ dependencies:
49
49
  type: :runtime
50
50
  version_requirements: *id002
51
51
  - !ruby/object:Gem::Dependency
52
- name: devise
52
+ name: actionmailer
53
53
  prerelease: false
54
54
  requirement: &id003 !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ hash: 7
60
+ segments:
61
+ - 3
62
+ - 0
63
+ version: "3.0"
64
+ type: :runtime
65
+ version_requirements: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ name: devise
68
+ prerelease: false
69
+ requirement: &id004 !ruby/object:Gem::Requirement
55
70
  none: false
56
71
  requirements:
57
72
  - - ">="
58
73
  - !ruby/object:Gem::Version
59
- hash: 15
74
+ hash: 11
60
75
  segments:
61
76
  - 2
77
+ - 1
62
78
  - 0
63
- - 0
64
- version: 2.0.0
79
+ version: 2.1.0
65
80
  type: :runtime
66
- version_requirements: *id003
81
+ version_requirements: *id004
67
82
  description: It adds support for send invitations by email (it requires to be authenticated) and accept the invitation by setting a password.
68
83
  email:
69
84
  - sergio@entrecables.com
@@ -79,21 +94,25 @@ files:
79
94
  - app/views/devise/invitations/new.html.erb
80
95
  - app/views/devise/mailer/invitation_instructions.html.erb
81
96
  - config/locales/en.yml
82
- - lib/devise_invitable/controllers/helpers.rb
83
- - lib/devise_invitable/controllers/registrations.rb
84
- - lib/devise_invitable/controllers/url_helpers.rb
85
- - lib/devise_invitable/inviter.rb
97
+ - lib/devise_invitable.rb
86
98
  - lib/devise_invitable/mailer.rb
87
99
  - lib/devise_invitable/model.rb
88
100
  - lib/devise_invitable/rails.rb
89
101
  - lib/devise_invitable/routes.rb
90
102
  - lib/devise_invitable/version.rb
91
- - lib/devise_invitable.rb
103
+ - lib/devise_invitable/controllers/helpers.rb
104
+ - lib/devise_invitable/controllers/url_helpers.rb
105
+ - lib/devise_invitable/controllers/registrations.rb
106
+ - lib/devise_invitable/controllers/registrations.rb~
107
+ - lib/devise_invitable/inviter.rb
108
+ - lib/devise_invitable/model.rb~
92
109
  - lib/generators/active_record/devise_invitable_generator.rb
93
110
  - lib/generators/active_record/templates/migration.rb
111
+ - lib/generators/devise_invitable/views_generator.rb
94
112
  - lib/generators/devise_invitable/devise_invitable_generator.rb
95
113
  - lib/generators/devise_invitable/install_generator.rb
96
- - lib/generators/devise_invitable/views_generator.rb
114
+ - lib/generators/devise_invitable/templates/simple_form_for/invitations/edit.html.erb
115
+ - lib/generators/devise_invitable/templates/simple_form_for/invitations/new.html.erb
97
116
  - lib/generators/mongoid/devise_invitable_generator.rb
98
117
  - LICENSE
99
118
  - README.rdoc
@@ -132,7 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
151
  requirements: []
133
152
 
134
153
  rubyforge_project:
135
- rubygems_version: 1.8.15
154
+ rubygems_version: 1.8.23
136
155
  signing_key:
137
156
  specification_version: 3
138
157
  summary: An invitation strategy for Devise