devise_invitable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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