aihs_devise_invitable 0.4.rc

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. data/.gitignore +27 -0
  2. data/Gemfile +8 -0
  3. data/Gemfile.lock +117 -0
  4. data/LICENSE +20 -0
  5. data/README.rdoc +187 -0
  6. data/Rakefile +57 -0
  7. data/app/controllers/devise/invitations_controller.rb +47 -0
  8. data/app/views/devise/invitations/edit.html.erb +14 -0
  9. data/app/views/devise/invitations/new.html.erb +12 -0
  10. data/app/views/devise/mailer/invitation.html.erb +8 -0
  11. data/app/views/devise/mailer/invitation_instructions.html.erb +8 -0
  12. data/config/locales/en.yml +9 -0
  13. data/devise_invitable.gemspec +143 -0
  14. data/lib/devise_invitable.rb +16 -0
  15. data/lib/devise_invitable/controllers/helpers.rb +7 -0
  16. data/lib/devise_invitable/controllers/url_helpers.rb +24 -0
  17. data/lib/devise_invitable/mailer.rb +14 -0
  18. data/lib/devise_invitable/model.rb +130 -0
  19. data/lib/devise_invitable/rails.rb +13 -0
  20. data/lib/devise_invitable/routes.rb +13 -0
  21. data/lib/devise_invitable/schema.rb +33 -0
  22. data/lib/devise_invitable/version.rb +3 -0
  23. data/lib/generators/active_record/devise_invitable_generator.rb +13 -0
  24. data/lib/generators/active_record/templates/migration.rb +20 -0
  25. data/lib/generators/devise_invitable/devise_invitable_generator.rb +16 -0
  26. data/lib/generators/devise_invitable/install_generator.rb +35 -0
  27. data/lib/generators/devise_invitable/views_generator.rb +10 -0
  28. data/test/generators_test.rb +45 -0
  29. data/test/integration/invitation_test.rb +107 -0
  30. data/test/integration_tests_helper.rb +58 -0
  31. data/test/mailers/invitation_mail_test.rb +63 -0
  32. data/test/model_tests_helper.rb +41 -0
  33. data/test/models/invitable_test.rb +213 -0
  34. data/test/models_test.rb +32 -0
  35. data/test/rails_app/app/controllers/admins_controller.rb +6 -0
  36. data/test/rails_app/app/controllers/application_controller.rb +3 -0
  37. data/test/rails_app/app/controllers/home_controller.rb +4 -0
  38. data/test/rails_app/app/controllers/users_controller.rb +12 -0
  39. data/test/rails_app/app/helpers/application_helper.rb +2 -0
  40. data/test/rails_app/app/models/octopussy.rb +11 -0
  41. data/test/rails_app/app/models/user.rb +4 -0
  42. data/test/rails_app/app/views/home/index.html.erb +0 -0
  43. data/test/rails_app/app/views/layouts/application.html.erb +16 -0
  44. data/test/rails_app/app/views/users/invitations/new.html.erb +15 -0
  45. data/test/rails_app/config.ru +4 -0
  46. data/test/rails_app/config/application.rb +46 -0
  47. data/test/rails_app/config/boot.rb +14 -0
  48. data/test/rails_app/config/database.yml +22 -0
  49. data/test/rails_app/config/environment.rb +5 -0
  50. data/test/rails_app/config/environments/development.rb +26 -0
  51. data/test/rails_app/config/environments/production.rb +49 -0
  52. data/test/rails_app/config/environments/test.rb +35 -0
  53. data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  54. data/test/rails_app/config/initializers/devise.rb +174 -0
  55. data/test/rails_app/config/initializers/inflections.rb +10 -0
  56. data/test/rails_app/config/initializers/mime_types.rb +5 -0
  57. data/test/rails_app/config/initializers/secret_token.rb +7 -0
  58. data/test/rails_app/config/initializers/session_store.rb +8 -0
  59. data/test/rails_app/config/locales/en.yml +5 -0
  60. data/test/rails_app/config/routes.rb +4 -0
  61. data/test/rails_app/script/rails +6 -0
  62. data/test/routes_test.rb +20 -0
  63. data/test/test_helper.rb +31 -0
  64. metadata +222 -0
@@ -0,0 +1,14 @@
1
+ <h2>Set your password</h2>
2
+
3
+ <%= 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
+ <p><%= f.label :password %><br />
8
+ <%= f.password_field :password %></p>
9
+
10
+ <p><%= f.label :password_confirmation %><br />
11
+ <%= f.password_field :password_confirmation %></p>
12
+
13
+ <p><%= f.submit "Set my password" %></p>
14
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <h2>Send invitation</h2>
2
+
3
+ <%= form_for resource, :as => resource_name, :url => invitation_path(resource_name) do |f| %>
4
+ <%= devise_error_messages! %>
5
+
6
+ <p><%= f.label :email %><br />
7
+ <%= f.text_field :email %></p>
8
+
9
+ <p><%= f.submit "Send an invitation" %></p>
10
+ <% end %>
11
+
12
+ <%= link_to "Home", after_sign_in_path_for(resource_name) %><br />
@@ -0,0 +1,8 @@
1
+ <p>Hello <%= @resource.email %>!</p>
2
+
3
+ <p>Someone has invited you to <%= root_url %>, you can accept it through the link below.</p>
4
+
5
+ <p><%= link_to 'Accept invitation', accept_invitation_url(@resource, :invitation_token => @resource.invitation_token) %></p>
6
+
7
+ <p>If you don't want to accept the invitation, please ignore this email.<br />
8
+ Your account won't be created until you access the link above and set your password.</p>
@@ -0,0 +1,8 @@
1
+ <p>Hello <%= @resource.email %>!</p>
2
+
3
+ <p>Someone has invited you to <%= root_url %>, you can accept it through the link below.</p>
4
+
5
+ <p><%= link_to 'Accept invitation', accept_invitation_url(@resource, :invitation_token => @resource.invitation_token) %></p>
6
+
7
+ <p>If you don't want to accept the invitation, please ignore this email.<br />
8
+ Your account won't be created until you access the link above and set your password.</p>
@@ -0,0 +1,9 @@
1
+ en:
2
+ devise:
3
+ invitations:
4
+ send_instructions: 'An invitation email has been sent to %{email}.'
5
+ invitation_token_invalid: 'The invitation token provided is not valid!'
6
+ updated: 'Your password was set successfully. You are now signed in.'
7
+ mailer:
8
+ invitation_instructions:
9
+ subject: 'Invitation instructions'
@@ -0,0 +1,143 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{devise_invitable}
8
+ s.version = "0.4.rc"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Sergio Cambra"]
12
+ s.date = %q{2010-10-05}
13
+ s.description = %q{It adds support for send invitations by email (it requires to be authenticated) and accept the invitation setting the password}
14
+ s.email = %q{sergio@entrecables.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "Gemfile",
23
+ "Gemfile.lock",
24
+ "LICENSE",
25
+ "README.rdoc",
26
+ "Rakefile",
27
+ "app/controllers/devise/invitations_controller.rb",
28
+ "app/views/devise/invitations/edit.html.erb",
29
+ "app/views/devise/invitations/new.html.erb",
30
+ "app/views/devise/mailer/invitation.html.erb",
31
+ "config/locales/en.yml",
32
+ "devise_invitable.gemspec",
33
+ "lib/devise_invitable.rb",
34
+ "lib/devise_invitable/controllers/helpers.rb",
35
+ "lib/devise_invitable/controllers/url_helpers.rb",
36
+ "lib/devise_invitable/mailer.rb",
37
+ "lib/devise_invitable/model.rb",
38
+ "lib/devise_invitable/rails.rb",
39
+ "lib/devise_invitable/routes.rb",
40
+ "lib/devise_invitable/schema.rb",
41
+ "lib/devise_invitable/version.rb",
42
+ "lib/generators/active_record/devise_invitable_generator.rb",
43
+ "lib/generators/active_record/templates/migration.rb",
44
+ "lib/generators/devise_invitable/devise_invitable_generator.rb",
45
+ "lib/generators/devise_invitable/install_generator.rb",
46
+ "lib/generators/devise_invitable/views_generator.rb",
47
+ "test/generators_test.rb",
48
+ "test/integration/invitation_test.rb",
49
+ "test/integration_tests_helper.rb",
50
+ "test/mailers/invitation_mail_test.rb",
51
+ "test/model_tests_helper.rb",
52
+ "test/models/invitable_test.rb",
53
+ "test/models_test.rb",
54
+ "test/rails_app/app/controllers/admins_controller.rb",
55
+ "test/rails_app/app/controllers/application_controller.rb",
56
+ "test/rails_app/app/controllers/home_controller.rb",
57
+ "test/rails_app/app/controllers/users_controller.rb",
58
+ "test/rails_app/app/helpers/application_helper.rb",
59
+ "test/rails_app/app/models/octopussy.rb",
60
+ "test/rails_app/app/models/user.rb",
61
+ "test/rails_app/app/views/home/index.html.erb",
62
+ "test/rails_app/app/views/layouts/application.html.erb",
63
+ "test/rails_app/app/views/users/invitations/new.html.erb",
64
+ "test/rails_app/config.ru",
65
+ "test/rails_app/config/application.rb",
66
+ "test/rails_app/config/boot.rb",
67
+ "test/rails_app/config/database.yml",
68
+ "test/rails_app/config/environment.rb",
69
+ "test/rails_app/config/environments/development.rb",
70
+ "test/rails_app/config/environments/production.rb",
71
+ "test/rails_app/config/environments/test.rb",
72
+ "test/rails_app/config/initializers/backtrace_silencers.rb",
73
+ "test/rails_app/config/initializers/devise.rb",
74
+ "test/rails_app/config/initializers/inflections.rb",
75
+ "test/rails_app/config/initializers/mime_types.rb",
76
+ "test/rails_app/config/initializers/secret_token.rb",
77
+ "test/rails_app/config/initializers/session_store.rb",
78
+ "test/rails_app/config/locales/en.yml",
79
+ "test/rails_app/config/routes.rb",
80
+ "test/rails_app/script/rails",
81
+ "test/routes_test.rb",
82
+ "test/test_helper.rb"
83
+ ]
84
+ s.homepage = %q{http://github.com/scambra/devise_invitable}
85
+ s.rdoc_options = ["--charset=UTF-8"]
86
+ s.require_paths = ["lib"]
87
+ s.rubygems_version = %q{1.3.7}
88
+ s.summary = %q{An invitation strategy for devise}
89
+ s.test_files = [
90
+ "test/generators_test.rb",
91
+ "test/integration/invitation_test.rb",
92
+ "test/integration_tests_helper.rb",
93
+ "test/mailers/invitation_mail_test.rb",
94
+ "test/model_tests_helper.rb",
95
+ "test/models/invitable_test.rb",
96
+ "test/models_test.rb",
97
+ "test/rails_app/app/controllers/admins_controller.rb",
98
+ "test/rails_app/app/controllers/application_controller.rb",
99
+ "test/rails_app/app/controllers/home_controller.rb",
100
+ "test/rails_app/app/controllers/users_controller.rb",
101
+ "test/rails_app/app/helpers/application_helper.rb",
102
+ "test/rails_app/app/models/octopussy.rb",
103
+ "test/rails_app/app/models/user.rb",
104
+ "test/rails_app/config/application.rb",
105
+ "test/rails_app/config/boot.rb",
106
+ "test/rails_app/config/environment.rb",
107
+ "test/rails_app/config/environments/development.rb",
108
+ "test/rails_app/config/environments/production.rb",
109
+ "test/rails_app/config/environments/test.rb",
110
+ "test/rails_app/config/initializers/backtrace_silencers.rb",
111
+ "test/rails_app/config/initializers/devise.rb",
112
+ "test/rails_app/config/initializers/inflections.rb",
113
+ "test/rails_app/config/initializers/mime_types.rb",
114
+ "test/rails_app/config/initializers/secret_token.rb",
115
+ "test/rails_app/config/initializers/session_store.rb",
116
+ "test/rails_app/config/routes.rb",
117
+ "test/routes_test.rb",
118
+ "test/test_helper.rb"
119
+ ]
120
+
121
+ if s.respond_to? :specification_version then
122
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
123
+ s.specification_version = 3
124
+
125
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
126
+ s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
127
+ s.add_development_dependency(%q<capybara>, [">= 0.3.9"])
128
+ s.add_development_dependency(%q<rails>, ["~> 3.0.0"])
129
+ s.add_development_dependency(%q<sqlite3-ruby>, [">= 0"])
130
+ else
131
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
132
+ s.add_dependency(%q<capybara>, [">= 0.3.9"])
133
+ s.add_dependency(%q<rails>, ["~> 3.0.0"])
134
+ s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
135
+ end
136
+ else
137
+ s.add_dependency(%q<mocha>, [">= 0.9.8"])
138
+ s.add_dependency(%q<capybara>, [">= 0.3.9"])
139
+ s.add_dependency(%q<rails>, ["~> 3.0.0"])
140
+ s.add_dependency(%q<sqlite3-ruby>, [">= 0"])
141
+ end
142
+ end
143
+
@@ -0,0 +1,16 @@
1
+ require 'devise'
2
+
3
+ require 'devise_invitable/mailer'
4
+ require 'devise_invitable/routes'
5
+ require 'devise_invitable/schema'
6
+ require 'devise_invitable/controllers/url_helpers'
7
+ require 'devise_invitable/controllers/helpers'
8
+ require 'devise_invitable/rails'
9
+
10
+ module Devise
11
+ # The period the generated invitation token is valid.
12
+ mattr_accessor :invite_for
13
+ @@invite_for = 0
14
+ end
15
+
16
+ Devise.add_module :invitable, :controller => :invitations, :model => 'devise_invitable/model', :route => :invitation
@@ -0,0 +1,7 @@
1
+ module DeviseInvitable::Controllers::Helpers
2
+ protected
3
+ def authenticate_inviter!
4
+ send(:"authenticate_#{resource_name}!")
5
+ end
6
+ end
7
+ ActionController::Base.send :include, DeviseInvitable::Controllers::Helpers
@@ -0,0 +1,24 @@
1
+ module DeviseInvitable
2
+ module Controllers
3
+ module UrlHelpers
4
+ [:path, :url].each do |path_or_url|
5
+ [nil, :new_, :accept_].each do |action|
6
+ class_eval <<-URL_HELPERS, __FILE__, __LINE__ + 1
7
+ def #{action}invitation_#{path_or_url}(resource, *args)
8
+ resource = case resource
9
+ when Symbol, String
10
+ resource
11
+ when Class
12
+ resource.name.underscore
13
+ else
14
+ resource.class.name.underscore
15
+ end
16
+
17
+ send("#{action}\#{resource}_invitation_#{path_or_url}", *args)
18
+ end
19
+ URL_HELPERS
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module DeviseInvitable
2
+ module Mailer
3
+
4
+ # Deliver an invitation email
5
+ def invitation_instructions(record)
6
+ setup_mail(record, :invitation_instructions)
7
+ end
8
+
9
+ def invitation(record)
10
+ ActiveSupport::Deprecation.warn('invitation has been renamed to invitation_instructions')
11
+ invitation_instructions(record)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,130 @@
1
+ module Devise
2
+ module Models
3
+ # Invitable is responsible for sending invitation emails.
4
+ # When an invitation is sent to an email address, an account is created for it.
5
+ # Invitation email contains a link allowing the user to accept the invitation
6
+ # by setting a password (as reset password from Devise's recoverable module).
7
+ #
8
+ # Configuration:
9
+ #
10
+ # invite_for: The period the generated invitation token is valid, after
11
+ # this period, the invited resource won't be able to accept the invitation.
12
+ # When invite_for is 0 (the default), the invitation won't expire.
13
+ #
14
+ # Examples:
15
+ #
16
+ # User.find(1).invited? # => true/false
17
+ # User.invite!(:email => 'someone@example.com') # => send invitation
18
+ # User.accept_invitation!(:invitation_token => '...') # => accept invitation with a token
19
+ # User.find(1).accept_invitation! # => accept invitation
20
+ # User.find(1).invite! # => reset invitation status and send invitation again
21
+ module Invitable
22
+ extend ActiveSupport::Concern
23
+
24
+ # Accept an invitation by clearing invitation token and confirming it if model
25
+ # is confirmable
26
+ def accept_invitation!
27
+ if self.invited? && self.valid?
28
+ self.invitation_token = nil
29
+ self.save
30
+ end
31
+ end
32
+
33
+ # Verifies whether a user has been invited or not
34
+ def invited?
35
+ persisted? && invitation_token.present?
36
+ end
37
+
38
+ # Reset invitation token and send invitation again
39
+ def invite!
40
+ if new_record? || invited?
41
+ self.skip_confirmation! if self.new_record? && self.respond_to?(:skip_confirmation!)
42
+ generate_invitation_token if self.invitation_token.nil?
43
+ self.invitation_sent_at = Time.now.utc
44
+ save(:validate => false)
45
+ ::Devise.mailer.invitation_instructions(self).deliver
46
+ end
47
+ end
48
+
49
+ # Verify whether a invitation is active or not. If the user has been
50
+ # invited, we need to calculate if the invitation time has not expired
51
+ # for this user, in other words, if the invitation is still valid.
52
+ def valid_invitation?
53
+ invited? && invitation_period_valid?
54
+ end
55
+
56
+ protected
57
+
58
+ # Checks if the invitation for the user is within the limit time.
59
+ # We do this by calculating if the difference between today and the
60
+ # invitation sent date does not exceed the invite for time configured.
61
+ # Invite_for is a model configuration, must always be an integer value.
62
+ #
63
+ # Example:
64
+ #
65
+ # # invite_for = 1.day and invitation_sent_at = today
66
+ # invitation_period_valid? # returns true
67
+ #
68
+ # # invite_for = 5.days and invitation_sent_at = 4.days.ago
69
+ # invitation_period_valid? # returns true
70
+ #
71
+ # # invite_for = 5.days and invitation_sent_at = 5.days.ago
72
+ # invitation_period_valid? # returns false
73
+ #
74
+ # # invite_for = nil
75
+ # invitation_period_valid? # will always return true
76
+ #
77
+ def invitation_period_valid?
78
+ invitation_sent_at && (self.class.invite_for.to_i.zero? || invitation_sent_at.utc >= self.class.invite_for.ago)
79
+ end
80
+
81
+ # Generates a new random token for invitation, and stores the time
82
+ # this token is being generated
83
+ def generate_invitation_token
84
+ self.invitation_token = self.class.invitation_token
85
+ end
86
+
87
+ module ClassMethods
88
+ # Attempt to find a user by it's email. If a record is not found, create a new
89
+ # user and send invitation to it. If user is found, returns the user with an
90
+ # email already exists error.
91
+ # Attributes must contain the user email, other attributes will be set in the record
92
+ def invite!(attributes={})
93
+ invitable = find_or_initialize_with_error_by(:email, attributes.delete(:email))
94
+ invitable.attributes = attributes
95
+
96
+ if invitable.new_record?
97
+ invitable.errors.clear if invitable.email.try(:match, Devise.email_regexp)
98
+ else
99
+ invitable.errors.add(:email, :taken) unless invitable.invited?
100
+ end
101
+
102
+ invitable.invite! if invitable.errors.empty?
103
+ invitable
104
+ end
105
+
106
+ # Attempt to find a user by it's invitation_token to set it's password.
107
+ # If a user is found, reset it's password and automatically try saving
108
+ # the record. If not user is found, returns a new user containing an
109
+ # error in invitation_token attribute.
110
+ # Attributes must contain invitation_token, password and confirmation
111
+ def accept_invitation!(attributes={})
112
+ invitable = find_or_initialize_with_error_by(:invitation_token, attributes.delete(:invitation_token))
113
+ invitable.errors.add(:invitation_token, :invalid) if invitable.invitation_token && invitable.persisted? && !invitable.valid_invitation?
114
+ if invitable.errors.empty?
115
+ invitable.attributes = attributes
116
+ invitable.accept_invitation!
117
+ end
118
+ invitable
119
+ end
120
+
121
+ # Generate a token checking if one does not already exist in the database.
122
+ def invitation_token
123
+ generate_token(:invitation_token)
124
+ end
125
+
126
+ Devise::Models.config(self, :invite_for)
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,13 @@
1
+ module DeviseInvitable
2
+ class Engine < ::Rails::Engine
3
+
4
+ ActiveSupport.on_load(:action_controller) { include DeviseInvitable::Controllers::UrlHelpers }
5
+ ActiveSupport.on_load(:action_view) { include DeviseInvitable::Controllers::UrlHelpers }
6
+
7
+ config.after_initialize do
8
+ require 'devise/mailer'
9
+ Devise::Mailer.send :include, DeviseInvitable::Mailer
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module ActionDispatch::Routing
2
+ class Mapper
3
+
4
+ protected
5
+ def devise_invitation(mapping, controllers)
6
+ resource :invitation, :only => [:new, :create, :update],
7
+ :path => mapping.path_names[:invitation], :controller => controllers[:invitations] do
8
+ get :edit, :path => mapping.path_names[:accept], :as => :accept
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ module DeviseInvitable
2
+ module Schema
3
+ # Add invitation_token and invitation_sent_at columns in the resource's database table.
4
+ #
5
+ # Examples
6
+ #
7
+ # # For a new resource migration:
8
+ # create_table :the_resources do
9
+ # t.database_authenticatable :null => false # you need at least this
10
+ # t.invitable
11
+ # ...
12
+ # end
13
+ # add_index :the_resources, :invitation_token # for invitable
14
+ #
15
+ # # or if the resource's table already exists, define a migration and put this in:
16
+ # change_table :the_resources do |t|
17
+ # t.string :invitation_token, :limit => 60
18
+ # t.datetime :invitation_sent_at
19
+ # t.index :invitation_token # for invitable
20
+ # end
21
+ #
22
+ # # And allow encrypted_password to be null:
23
+ # change_column :the_resources, :encrypted_password, :string, :null => true
24
+ # # the following line is only if you use Devise's encryptable module!
25
+ # change_column :the_resources, :password_salt, :string, :null => true
26
+ def invitable
27
+ apply_devise_schema :invitation_token, String, :limit => 60
28
+ apply_devise_schema :invitation_sent_at, DateTime
29
+ end
30
+ end
31
+ end
32
+
33
+ Devise::Schema.send :include, DeviseInvitable::Schema