aihs_devise_invitable 0.4.rc

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.
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