devise_invitable 0.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.
Files changed (49) hide show
  1. data/.document +5 -0
  2. data/.gitignore +22 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +54 -0
  6. data/VERSION +1 -0
  7. data/app/controllers/invitations_controller.rb +48 -0
  8. data/app/views/devise_mailer/invitation.html.erb +8 -0
  9. data/app/views/invitations/edit.html.erb +14 -0
  10. data/app/views/invitations/new.html.erb +10 -0
  11. data/devise_invitable.gemspec +121 -0
  12. data/init.rb +1 -0
  13. data/lib/devise/controllers/url_helpers.rb +20 -0
  14. data/lib/devise/models/invitable.rb +143 -0
  15. data/lib/devise_invitable.rb +14 -0
  16. data/lib/devise_invitable/locales/en.yml +5 -0
  17. data/lib/devise_invitable/mailer.rb +9 -0
  18. data/lib/devise_invitable/rails.rb +3 -0
  19. data/lib/devise_invitable/routes.rb +28 -0
  20. data/lib/devise_invitable/schema.rb +11 -0
  21. data/rails/init.rb +1 -0
  22. data/test/integration/invitable_test.rb +122 -0
  23. data/test/integration_tests_helper.rb +38 -0
  24. data/test/mailers/invitation_test.rb +62 -0
  25. data/test/model_tests_helper.rb +59 -0
  26. data/test/models/invitable_test.rb +164 -0
  27. data/test/models_test.rb +35 -0
  28. data/test/rails_app/app/controllers/admins_controller.rb +6 -0
  29. data/test/rails_app/app/controllers/application_controller.rb +10 -0
  30. data/test/rails_app/app/controllers/home_controller.rb +4 -0
  31. data/test/rails_app/app/controllers/users_controller.rb +12 -0
  32. data/test/rails_app/app/helpers/application_helper.rb +3 -0
  33. data/test/rails_app/app/models/user.rb +4 -0
  34. data/test/rails_app/app/views/home/index.html.erb +0 -0
  35. data/test/rails_app/config/boot.rb +110 -0
  36. data/test/rails_app/config/database.yml +22 -0
  37. data/test/rails_app/config/environment.rb +44 -0
  38. data/test/rails_app/config/environments/development.rb +17 -0
  39. data/test/rails_app/config/environments/production.rb +28 -0
  40. data/test/rails_app/config/environments/test.rb +28 -0
  41. data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  42. data/test/rails_app/config/initializers/inflections.rb +2 -0
  43. data/test/rails_app/config/initializers/new_rails_defaults.rb +21 -0
  44. data/test/rails_app/config/initializers/session_store.rb +15 -0
  45. data/test/rails_app/config/routes.rb +3 -0
  46. data/test/rails_app/vendor/plugins/devise_invitable/init.rb +1 -0
  47. data/test/routes_test.rb +20 -0
  48. data/test/test_helper.rb +58 -0
  49. metadata +156 -0
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ test/rails_app/log
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Sergio Cambra
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,18 @@
1
+ = devise_invitable
2
+
3
+ It adds support for send invitations by email (it requires to be authenticated) and accept the invitation setting the password.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but
13
+ bump version in a commit by itself I can ignore when I pull)
14
+ * Send me a pull request. Bonus points for topic branches.
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2009 Sergio Cambra. See LICENSE for details.
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "devise_invitable"
8
+ gem.summary = %Q{An invitation strategy for devise}
9
+ gem.description = %Q{It adds support for send invitations by email (it requires to be authenticated) and accept the invitation setting the password}
10
+ gem.email = "sergio@entrecables.com"
11
+ gem.homepage = "http://github.com/scambra/devise_invitable"
12
+ gem.authors = ["Sergio Cambra"]
13
+ gem.add_development_dependency 'mocha'
14
+ gem.add_development_dependency 'webrat'
15
+ gem.add_dependency 'devise', '>= 0.7.1'
16
+ end
17
+ Jeweler::GemcutterTasks.new
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'rake/testtask'
23
+ Rake::TestTask.new(:test) do |test|
24
+ test.libs << 'lib' << 'test'
25
+ test.pattern = 'test/**/*_test.rb'
26
+ test.verbose = true
27
+ end
28
+
29
+ begin
30
+ require 'rcov/rcovtask'
31
+ Rcov::RcovTask.new do |test|
32
+ test.libs << 'test'
33
+ test.pattern = 'test/**/test_*.rb'
34
+ test.verbose = true
35
+ end
36
+ rescue LoadError
37
+ task :rcov do
38
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
39
+ end
40
+ end
41
+
42
+ task :test => :check_dependencies
43
+
44
+ task :default => :test
45
+
46
+ require 'rake/rdoctask'
47
+ Rake::RDocTask.new do |rdoc|
48
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
49
+
50
+ rdoc.rdoc_dir = 'rdoc'
51
+ rdoc.title = "devise_invitable #{version}"
52
+ rdoc.rdoc_files.include('README*')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,48 @@
1
+ class InvitationsController < ApplicationController
2
+ include Devise::Controllers::Helpers
3
+
4
+ before_filter :authenticate_resource!, :only => [:new, :create]
5
+ before_filter :require_no_authentication, :only => [:edit, :update]
6
+
7
+ # GET /resource/invitation/new
8
+ def new
9
+ build_resource
10
+ render_with_scope :new
11
+ end
12
+
13
+ # POST /resource/invitation
14
+ def create
15
+ self.resource = resource_class.send_invitation(params[resource_name])
16
+
17
+ if resource.errors.empty?
18
+ set_flash_message :success, :send_invitation
19
+ redirect_to after_sign_in_path_for(resource_name)
20
+ else
21
+ render_with_scope :new
22
+ end
23
+ end
24
+
25
+ # GET /resource/invitation/edit?invitation_token=abcdef
26
+ def edit
27
+ self.resource = resource_class.new
28
+ resource.invitation_token = params[:invitation_token]
29
+ render_with_scope :edit
30
+ end
31
+
32
+ # PUT /resource/invitation
33
+ def update
34
+ self.resource = resource_class.accept_invitation!(params[resource_name])
35
+
36
+ if resource.errors.empty?
37
+ set_flash_message :success, :updated
38
+ sign_in_and_redirect(resource_name, resource)
39
+ else
40
+ render_with_scope :edit
41
+ end
42
+ end
43
+
44
+ protected
45
+ def authenticate_resource!
46
+ authenticate!(resource_name)
47
+ end
48
+ end
@@ -0,0 +1,8 @@
1
+ Hello <%= @resource.email %>!
2
+
3
+ Someone has invited you to <%= root_url %>, you can accept it through the link below.
4
+
5
+ <%= link_to 'Accept invitation', edit_invitation_url(@resource, :invitation_token => @resource.invitation_token) %>
6
+
7
+ If you don't want to accept the invitation, please ignore this email.
8
+ Your account won't be created until you access the link above and set your password.
@@ -0,0 +1,14 @@
1
+ <h2>Set your password</h2>
2
+
3
+ <% form_for resource_name, resource, :url => invitation_path(resource_name), :html => { :method => :put } do |f| %>
4
+ <%= f.error_messages %>
5
+ <%= f.hidden_field :invitation_token %>
6
+
7
+ <p><%= f.label :password %></p>
8
+ <p><%= f.password_field :password %></p>
9
+
10
+ <p><%= f.label :password_confirmation %></p>
11
+ <p><%= f.password_field :password_confirmation %></p>
12
+
13
+ <p><%= f.submit "Set my password" %></p>
14
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <h2>Send invitation</h2>
2
+
3
+ <% form_for resource_name, resource, :url => invitation_path(resource_name) do |f| %>
4
+ <%= f.error_messages %>
5
+
6
+ <p><%= f.label :email %></p>
7
+ <p><%= f.text_field :email %></p>
8
+
9
+ <p><%= f.submit "Send an invitation" %></p>
10
+ <% end %>
@@ -0,0 +1,121 @@
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.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Sergio Cambra"]
12
+ s.date = %q{2009-12-10}
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
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "app/controllers/invitations_controller.rb",
27
+ "app/views/devise_mailer/invitation.html.erb",
28
+ "app/views/invitations/edit.html.erb",
29
+ "app/views/invitations/new.html.erb",
30
+ "devise_invitable.gemspec",
31
+ "init.rb",
32
+ "lib/devise/controllers/url_helpers.rb",
33
+ "lib/devise/models/invitable.rb",
34
+ "lib/devise_invitable.rb",
35
+ "lib/devise_invitable/locales/en.yml",
36
+ "lib/devise_invitable/mailer.rb",
37
+ "lib/devise_invitable/rails.rb",
38
+ "lib/devise_invitable/routes.rb",
39
+ "lib/devise_invitable/schema.rb",
40
+ "rails/init.rb",
41
+ "test/integration/invitable_test.rb",
42
+ "test/integration_tests_helper.rb",
43
+ "test/mailers/invitation_test.rb",
44
+ "test/model_tests_helper.rb",
45
+ "test/models/invitable_test.rb",
46
+ "test/models_test.rb",
47
+ "test/rails_app/app/controllers/admins_controller.rb",
48
+ "test/rails_app/app/controllers/application_controller.rb",
49
+ "test/rails_app/app/controllers/home_controller.rb",
50
+ "test/rails_app/app/controllers/users_controller.rb",
51
+ "test/rails_app/app/helpers/application_helper.rb",
52
+ "test/rails_app/app/models/user.rb",
53
+ "test/rails_app/app/views/home/index.html.erb",
54
+ "test/rails_app/config/boot.rb",
55
+ "test/rails_app/config/database.yml",
56
+ "test/rails_app/config/environment.rb",
57
+ "test/rails_app/config/environments/development.rb",
58
+ "test/rails_app/config/environments/production.rb",
59
+ "test/rails_app/config/environments/test.rb",
60
+ "test/rails_app/config/initializers/backtrace_silencers.rb",
61
+ "test/rails_app/config/initializers/inflections.rb",
62
+ "test/rails_app/config/initializers/new_rails_defaults.rb",
63
+ "test/rails_app/config/initializers/session_store.rb",
64
+ "test/rails_app/config/routes.rb",
65
+ "test/rails_app/vendor/plugins/devise_invitable/init.rb",
66
+ "test/routes_test.rb",
67
+ "test/test_helper.rb"
68
+ ]
69
+ s.homepage = %q{http://github.com/scambra/devise_invitable}
70
+ s.rdoc_options = ["--charset=UTF-8"]
71
+ s.require_paths = ["lib"]
72
+ s.rubygems_version = %q{1.3.5}
73
+ s.summary = %q{An invitation strategy for devise}
74
+ s.test_files = [
75
+ "test/model_tests_helper.rb",
76
+ "test/integration_tests_helper.rb",
77
+ "test/models_test.rb",
78
+ "test/mailers/invitation_test.rb",
79
+ "test/routes_test.rb",
80
+ "test/integration/invitable_test.rb",
81
+ "test/models/invitable_test.rb",
82
+ "test/test_helper.rb",
83
+ "test/rails_app/app/controllers/application_controller.rb",
84
+ "test/rails_app/app/controllers/admins_controller.rb",
85
+ "test/rails_app/app/controllers/home_controller.rb",
86
+ "test/rails_app/app/controllers/users_controller.rb",
87
+ "test/rails_app/app/models/user.rb",
88
+ "test/rails_app/app/helpers/application_helper.rb",
89
+ "test/rails_app/config/routes.rb",
90
+ "test/rails_app/config/initializers/backtrace_silencers.rb",
91
+ "test/rails_app/config/initializers/new_rails_defaults.rb",
92
+ "test/rails_app/config/initializers/session_store.rb",
93
+ "test/rails_app/config/initializers/inflections.rb",
94
+ "test/rails_app/config/environments/test.rb",
95
+ "test/rails_app/config/environments/development.rb",
96
+ "test/rails_app/config/environments/production.rb",
97
+ "test/rails_app/config/environment.rb",
98
+ "test/rails_app/config/boot.rb",
99
+ "test/rails_app/vendor/plugins/devise_invitable/init.rb"
100
+ ]
101
+
102
+ if s.respond_to? :specification_version then
103
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
104
+ s.specification_version = 3
105
+
106
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
107
+ s.add_development_dependency(%q<mocha>, [">= 0"])
108
+ s.add_development_dependency(%q<webrat>, [">= 0"])
109
+ s.add_runtime_dependency(%q<devise>, [">= 0.7.1"])
110
+ else
111
+ s.add_dependency(%q<mocha>, [">= 0"])
112
+ s.add_dependency(%q<webrat>, [">= 0"])
113
+ s.add_dependency(%q<devise>, [">= 0.7.1"])
114
+ end
115
+ else
116
+ s.add_dependency(%q<mocha>, [">= 0"])
117
+ s.add_dependency(%q<webrat>, [">= 0"])
118
+ s.add_dependency(%q<devise>, [">= 0.7.1"])
119
+ end
120
+ end
121
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'rails/init'
@@ -0,0 +1,20 @@
1
+ Devise::Controllers::UrlHelpers.module_eval do
2
+ [:path, :url].each do |path_or_url|
3
+ [nil, :new_, :edit_].each do |action|
4
+ class_eval <<-URL_HELPERS
5
+ def #{action}invitation_#{path_or_url}(resource, *args)
6
+ resource = case resource
7
+ when Symbol, String
8
+ resource
9
+ when Class
10
+ resource.name.underscore
11
+ else
12
+ resource.class.name.underscore
13
+ end
14
+
15
+ send("#{action}\#{resource}_invitation_#{path_or_url}", *args)
16
+ end
17
+ URL_HELPERS
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,143 @@
1
+ module Devise
2
+ module Models
3
+
4
+ # Invitable is responsible to send emails with invitations.
5
+ # When an invitation is sent to an email, an account is created for it.
6
+ # An invitation has a link to set the password, as reset password from recoverable.
7
+ #
8
+ # Configuration:
9
+ #
10
+ # invite_for: the time you want the user will have to confirm the account after
11
+ # is invited. When invite_for is zero, the invitation won't expire.
12
+ # By default invite_for is 0.
13
+ #
14
+ # Examples:
15
+ #
16
+ # User.find(1).invited? # true/false
17
+ # User.send_invitation(: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).reset_invitation! # reset invitation status and send invitation again
21
+ module Invitable
22
+
23
+ def self.included(base)
24
+ base.class_eval do
25
+ extend ClassMethods
26
+ callbacks = []
27
+ callbacks.concat before_create_callback_chain.select {|c| c.method == :generate_confirmation_token}
28
+ callbacks.concat after_create_callback_chain.select {|c| c.method == :send_confirmation_instructions}
29
+ callbacks.each do |c|
30
+ c.options[:unless] = lambda{|r| r.class.devise_modules.include?(:invitable) && r.invitation_token}
31
+ end
32
+ end
33
+ end
34
+
35
+ # Accept an invitation by clearing invitation token and confirming it if model
36
+ # is confirmable
37
+ def accept_invitation!
38
+ if self.invited?
39
+ self.invitation_token = nil
40
+ if self.class.devise_modules.include? :confirmable
41
+ self.confirm!
42
+ else
43
+ save
44
+ end
45
+ end
46
+ end
47
+
48
+ # Verifies whether a user has been invited or not
49
+ def invited?
50
+ !new_record? && !invitation_token.nil?
51
+ end
52
+
53
+ # Send invitation by email
54
+ def send_invitation
55
+ ::DeviseMailer.deliver_invitation(self)
56
+ end
57
+
58
+ # Reset invitation token and send invitation again
59
+ def reset_invitation!
60
+ if new_record? || invited?
61
+ generate_invitation_token
62
+ save(false)
63
+ send_invitation
64
+ end
65
+ end
66
+
67
+ # Verify whether a invitation is active or not. If the user has been
68
+ # invited, we need to calculate if the invitation time has not expired
69
+ # for this user, in other words, if the invitation is still valid.
70
+ def valid_invitation?
71
+ invited? && invitation_period_valid?
72
+ end
73
+
74
+ protected
75
+
76
+ # Checks if the invitation for the user is within the limit time.
77
+ # We do this by calculating if the difference between today and the
78
+ # invitation sent date does not exceed the invite for time configured.
79
+ # Invite_for is a model configuration, must always be an integer value.
80
+ #
81
+ # Example:
82
+ #
83
+ # # invite_for = 1.day and invitation_sent_at = today
84
+ # invitation_period_valid? # returns true
85
+ #
86
+ # # invite_for = 5.days and invitation_sent_at = 4.days.ago
87
+ # invitation_period_valid? # returns true
88
+ #
89
+ # # invite_for = 5.days and invitation_sent_at = 5.days.ago
90
+ # invitation_period_valid? # returns false
91
+ #
92
+ # # invite_for = nil
93
+ # invitation_period_valid? # will always return true
94
+ #
95
+ def invitation_period_valid?
96
+ invitation_sent_at && (self.class.invite_for.to_i.zero? || invitation_sent_at.utc >= self.class.invite_for.ago)
97
+ end
98
+
99
+ # Generates a new random token for invitation, and stores the time
100
+ # this token is being generated
101
+ def generate_invitation_token
102
+ self.invitation_token = Devise.friendly_token
103
+ self.invitation_sent_at = Time.now.utc
104
+ end
105
+
106
+ module ClassMethods
107
+ # Attempt to find a user by it's email. If a record is not found, create a new
108
+ # user and send invitation to it. If user is found, returns the user with an
109
+ # email already exists error.
110
+ # Options must contain the user email
111
+ def send_invitation(attributes={})
112
+ invitable = find_or_initialize_by_email(attributes[:email])
113
+ if invitable.email.blank?
114
+ invitable.errors.add(:email, :blank)
115
+ elsif invitable.new_record? || invitable.invited?
116
+ invitable.reset_invitation!
117
+ else
118
+ invitable.errors.add(:email, :already_exits, :default => 'already exists')
119
+ end
120
+ invitable
121
+ end
122
+
123
+ # Attempt to find a user by it's invitation_token to set it's password.
124
+ # If a user is found, reset it's password and automatically try saving
125
+ # the record. If not user is found, returns a new user containing an
126
+ # error in invitation_token attribute.
127
+ # Attributes must contain invitation_token, password and confirmation
128
+ def accept_invitation!(attributes={})
129
+ invitable = find_or_initialize_with_error_by(:invitation_token, attributes[:invitation_token])
130
+ invitable.errors.add(:invitation_token, :invalid) if attributes[:invitation_token] && !invitable.new_record? && !invitable.valid_invitation?
131
+ if invitable.errors.empty?
132
+ invitable.password = attributes[:password]
133
+ invitable.password_confirmation = attributes[:password_confirmation]
134
+ invitable.accept_invitation! if invitable.valid?
135
+ end
136
+ invitable
137
+ end
138
+
139
+ Devise::Models.config(self, :invite_for)
140
+ end
141
+ end
142
+ end
143
+ end