@Ryan Bates: Aha, I see. Didn't recognize it. Thanks.
Once again, great stuff. I began with rails around new years 2007/8 and most of the stuff here have inspired my code and my own little conventions to the better.
I have a question with regard to associations in the invitation model. Sorry to be a bore, but I am not getting this relationship at all;
belongs_to :sender, :class_name => 'User'
has_one :recipient, :class_name => 'User'
How does a :sender and a :recipient relate to a class 'User'? Is it through virtual attr? I just can see this connection.
You mention that you are sponsored by pragmatic.tv and I've tried several times to go there with no luck. Do you know what's up (or down) with their site?
@Alek
Rails would look for a sender and recipient model ... but as ryan did not define a separate sender and recipient model you'll get an error.
for cases where you just want a different name then "invitation.user" and use it for different meanings ryan defined the class attribute to tell rails that invitation.sender and invitation.recipient are user-model objects. hope this helps.
@Alek, this structure allows you to call something like @invitation.sender and know who the sender is, and @invitation.recipient and know who the recipient is, even though both are rows in the 'User' table. It's basically like an alias for each relationship.
@QuBiT and @Carl; Thanks for kind respond and an explanation.
I am still not getting it. Sorry:-(. Is the :sender and the :recipient a virtual attribute? I am not seeing any hook methods pointing to the :recipient and the :sender.
Does the model, in this case User, receives the invitations' id and it knows who the sender is?
@Carl, when you say an alias do you mean it belongs to that particular model...in this case User?
Congrats Ryan - lots of information clearly presented.
@Alek - it isn't a virtual attribute. Take a look at the API for belongs_to (http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/belongs_to). This also works in has_many and has_and_belongs_to_many.
It is a shorthand way to tell the model that you want to connection to another other model, access it as sender, and look for something in User with a related sender_id column. It does all of that in one line. Good luck.
Thanks for an excellent episode, one of the most in-depth so far.
If anyone plans to put this in production, there's a couple edge cases to examine. Consider adding an
attr_accessible :recipient_email
to the invitation class (It looks like everything that could be mass-assigned is handled downstream, but its absence makes me nervous), and pulling :invitation_token from the call to User.attr_accessible (if you later add a user update action its value could be crafted: I could be my own grandkid). If a user can be deleted I think their invite codes could re-submit (&so multiply). Also, the presence of multiple routes to the users/new action looks like a problem here, one that should probably be warned about in restful_authentication.
These are just nitpicks for anyone who might build on this excellent foundation. Clearly this episode was already remarkably deep without these ponderous details.
While I am getting the error shown in the video when trying to register an account with an incorrect invitation ("invitation required"), the video doesn't show a successful registration with the code being passed into the form. I can't get the new User object to pick up the invitation_token. If I can assign it to the a different field (email, for instance) and it shows up, but the getter/setter methods seem to be interfering with it.
@Philip, thanks for pointing out those details, and I agree with you. There are definitely some extra things you need to consider to polish this up for your site before going into production.
@Michael, did you add invitation_token to the attr_accessible line in your User model?
will display the invitation_key in the email field or any other EXCEPT the invitation_key field (hidden or otherwise). Removing the getter/setter fixes this, though.
@greg there is an alternative podcast feed in iTunes -- follow the itunes link at top of page, then look for 'Railscasts (iPod + Apple TV)' in "Also Subscribed To"
Thanks for the excellent cast, it was really useful. One minor point, it might be useful to change 'validate :recipient_is_not_registered' in Invitation to 'validate_on_create :recipient_is_not_registered'; once a user is inevitably created, it seems that this would invalidate its associated invitation. This is pretty minor considering that you don't typically need to modify invitation objects after a user has accepted, but just in case...
I believe the user model should be "validates_presence_of :invitation_id, :message => "is required.", :on => :create". By doing this the model will only validate that the user has an invitation on creation rather than any time the user updates anything on there profile.
great episode. I have a question that has actually come up in certain other episodes. I understand the difference between class and instance methods. But I dont understand why in the setter method of invitation_token= you set
self.invitation=Invitation.find_by_token(token)
why is it the class variable there as in self.invitation, isn't each invitation a separate instance?
Thank you!
(I am looking at around the 16:24 minute mark of this video)
I am getting nil values for :sender in the invitation model when trying to create an invite.
It seems like there's something wrong with my relationship between the user and invitation models, even though I copied your relationships in the screencast.
Awesome railscast Ryan. Thanks. Used it right away on a project.
I didn't have beta invitations previously in the project, but wanted to switch to them. I ran into a problem with updating the contents of old users (since they didn't have an invitation id and since the validation still required it).
For those of you having a similar problem my solution was to add an unless statement Proc on the validation.
The code as implemented here seems to be able to handle bogus tokens (e.g., "123"), but not the absence of a token -- when I point my browser to localhost:3000/signup/ I get
Routing Error
No route matches "/signup/" with {:method=>:get}
Is there an easy way to remedy this?
Perhaps when no token is supplied, the behavior can be to go to that "add your email"-type page detailed at the end...? But more importantly, how to avoid the crash?
Doing this allowed me to prevent my previous links to the standard signup page from throwing errors, whilst haveing the new _with_token route for the invites. Thanks for a great episode, Ryan!
adding the "join the beta" page, I noticed that when there's a validation error, it shows up back on the normal "new invitation" page. any way to do that differently?
This has been very helpful! I've made a couple tweaks to make this work for a social network, so the invited user gets auto-friended with the person who invited him, and gets pending requests from other people who may have invited him. Maybe once I get everything squared away, I'll post some of my code.
One concern I have is if the invitation's token is guaranteed to be unique. Perhaps I'm being paranoid, but I think I'm going to modify this to use the invitation's id & token to uniquely grab the invite. Thoughts?
To connect to gmail, use the plugin
http://github.com/collectiveidea/action_mailer_optional_tls
It has been updated to support Ruby 1.8.6 and 1.8.7 both.
How could I combine this (would I combine this) with the "add a friend" functionality you described? I want a user to be able to invite someone via email and that invited person when joining is now a friend.
hello, GREAT tutorial.
I've got about half way though, where i need to test the invitaiton, and invite count decrements as expected. It handles missing email field, but when I enter an email address that is not listed already, I get:
undefined method `generate_token' for #<Invitation:0x2563bf8>
The invitation_controller has this:
@invitation.sender = @user
i used @user and user but bith return the same issue.
it would be great to do an update of this episode, so it works nicely together with Devise (see episodes 209,210).
When generating an Model through
Devise, Devise doesn't create a user_controller out of the box.. maybe you could put a note in here, on how to deal with that when trying to implement Beta-Invitations with Devise
I use Devise and have been trying to implement invitations. I am having an issues, can you help?
I can't get my devise model (user) to recognize the :invitation_token in the (new) method as pointed out in the episode, the hidden_field in the form never receives the invitation_token. It does get passed as a parameter though, I see it in logs.
Placing the :invitation_token and email assignments in the user_controller didn't work. I also created a registrations_controller and tried to override the new method. This didn't work either:
This might be too late for you, but I had this same issue and figured I would post the solution in case it is helpful to someone else out there.
For the Registrations controller, I had to create a "Users" folder in my controllers folder and then put the registrations_controller code below into my new "users/registrations_controller.rb" file (i.e. which inherits from the Devise registrations_controller). To understand why, take a look at the Devise registrations_controller "new" code on Github - essentially, the code that you added into your registrations_controller "new" section needs to be adjusted to use "resource" instead of "@user" and then it needs to run after the resource is built (ie. "build_resource({})"), but before the devise response code (i.e "respond_with self.resource").
For the controllers/users/registrations_controller.rb file:
class Users::RegistrationsController < Devise::RegistrationsController
def new
super
end
def create
super
end
def build_resource(hash=nil)
super(hash)
resource.invitation_token = params[:invitation_token]
resource.email = resource.invitation.recipient_email if resource.invitation
end
end
For the routes.rb file:
...
devise_for :users, :controllers => { :registrations => 'users/registrations' }, :action => 'new' do
get 'users/sign_up/:invitation_token' => 'users/registrations#new'
end
...
For the invitation_controller.rb file:
class InvitationsController < ApplicationController
def new
@invitation = Invitation.new
end
def create
@invitation = Invitation.new(params[:invitation])
@invitation.sender = current_user
if @invitation.save
if signed_in?
@invitation.update_attribute(:sent_at, Time.now)
Mailer.invitation(@invitation).deliver
flash[:notice] = "Thank you, your invitation has been sent."
redirect_to new_invitation_url
else
flash[:notice] = "Thank you, we will notify you when we are ready."
redirect_to root_url
end
else
render :action => 'new'
end
end
end
Update regarding the registrations controller code I posted above - better to put the added code in "new" vs within "build resource':
class Users::RegistrationsController < Devise::RegistrationsController
def new
build_resource({})
resource.invitation_token = params[:invitation_token]
resource.email = resource.invitation.recipient_email if resource.invitation
respond_with self.resource
end
How can i create an initial User ? If i try to create the initial User I get SQLite3::SQLException: no such column: users.invitation_id: SELECT 1 AS one FROM "users" WHERE "users"."invitation_id" IS NULL LIMIT 1 .. SO first User does not need an invitation how can i make an exception?
Very nice railscast!
Sorry if this is a little off-topic but I noticed you've done something nifty with the authentication system.
include Authentication
include Authentication::ByPassword
include Authentication::ByCookieToken
Is that child classes in the authentication.rb file to give it more flexibility?
Kind regards
Aerpe
@Aerpe, that is generated by restful_authentication. I believe those are just 3 modules they use to handle the authentication.
@Ryan Bates: Aha, I see. Didn't recognize it. Thanks.
Once again, great stuff. I began with rails around new years 2007/8 and most of the stuff here have inspired my code and my own little conventions to the better.
Ryan:
Thanks for a terrific podcast again.
I have a question with regard to associations in the invitation model. Sorry to be a bore, but I am not getting this relationship at all;
belongs_to :sender, :class_name => 'User'
has_one :recipient, :class_name => 'User'
How does a :sender and a :recipient relate to a class 'User'? Is it through virtual attr? I just can see this connection.
Thanks Ryan.
-Alek
Thanks for another great screencast.
You mention that you are sponsored by pragmatic.tv and I've tried several times to go there with no luck. Do you know what's up (or down) with their site?
Ryan, the video sizes of this episode r inversed :
Download (28.5 MB, 22:17)
alternative download for iPod & Apple TV (39.3 MB, 22:17)
@Ryan,
thx for this really useful tutorial.
@Alek
Rails would look for a sender and recipient model ... but as ryan did not define a separate sender and recipient model you'll get an error.
for cases where you just want a different name then "invitation.user" and use it for different meanings ryan defined the class attribute to tell rails that invitation.sender and invitation.recipient are user-model objects. hope this helps.
@Alek, this structure allows you to call something like @invitation.sender and know who the sender is, and @invitation.recipient and know who the recipient is, even though both are rows in the 'User' table. It's basically like an alias for each relationship.
QuBiT beat me to it!
@Per Velschow, hmm, it's working for me. Going to pragmatic.tv should redirect you to:
http://www.pragprog.com/screencasts
@khelll, thanks fixed.
@QuBiT and @Carl; Thanks for kind respond and an explanation.
I am still not getting it. Sorry:-(. Is the :sender and the :recipient a virtual attribute? I am not seeing any hook methods pointing to the :recipient and the :sender.
Does the model, in this case User, receives the invitations' id and it knows who the sender is?
@Carl, when you say an alias do you mean it belongs to that particular model...in this case User?
Thanks Guys!
-Alek
Thanks Ryan for this great episode.
I love this podcast so much !
Congrats Ryan - lots of information clearly presented.
@Alek - it isn't a virtual attribute. Take a look at the API for belongs_to (http://apidock.com/rails/ActiveRecord/Associations/ClassMethods/belongs_to). This also works in has_many and has_and_belongs_to_many.
It is a shorthand way to tell the model that you want to connection to another other model, access it as sender, and look for something in User with a related sender_id column. It does all of that in one line. Good luck.
@mpearce, Thank you! This is a terrific help, and to rest of you guys! Thanks!
A
I love you! This is exactly what I've been wanting to implement in my app. Its great that you listen to your readers suggestions. Love uservoce. :)
I'm getting stuck on the validations in the Invitation model. Everything was working fine and I've isolated this line as the problem.
(in models/invitation.rb)
validate :sender_has_invitations, :if => :sender
I get this error:
Validations need to be either a symbol, string (to be eval'ed), proc/method, or class implementing a static validation method
Please help!
Great screencast, Ryan. Now you left yourself to show us how to write the rake task that sends out the next 100 invites :)
Thanks for an excellent episode, one of the most in-depth so far.
If anyone plans to put this in production, there's a couple edge cases to examine. Consider adding an
attr_accessible :recipient_email
to the invitation class (It looks like everything that could be mass-assigned is handled downstream, but its absence makes me nervous), and pulling :invitation_token from the call to User.attr_accessible (if you later add a user update action its value could be crafted: I could be my own grandkid). If a user can be deleted I think their invite codes could re-submit (&so multiply). Also, the presence of multiple routes to the users/new action looks like a problem here, one that should probably be warned about in restful_authentication.
These are just nitpicks for anyone who might build on this excellent foundation. Clearly this episode was already remarkably deep without these ponderous details.
While I am getting the error shown in the video when trying to register an account with an incorrect invitation ("invitation required"), the video doesn't show a successful registration with the code being passed into the form. I can't get the new User object to pick up the invitation_token. If I can assign it to the a different field (email, for instance) and it shows up, but the getter/setter methods seem to be interfering with it.
I should add I'm using Rails 2.1
@matt, are you using Rails 2.1?
@Philip, thanks for pointing out those details, and I agree with you. There are definitely some extra things you need to consider to polish this up for your site before going into production.
@Michael, did you add invitation_token to the attr_accessible line in your User model?
@ryanb, Yes.
def new
@user.email = @user.invitation_key
...
will display the invitation_key in the email field or any other EXCEPT the invitation_key field (hidden or otherwise). Removing the getter/setter fixes this, though.
Of course not. That would probably solve my problem. :-)
@Michael, hmm, I'd have to see the code. Could you email me the user.rb file? ryan AT railscasts DOT com.
Works fine in iTunes on my Mac, but won't download to my iPhone 3G. No doubt someone has commented on this under another podcast.
These podcasts are great. Thanks.
@greg there is an alternative podcast feed in iTunes -- follow the itunes link at top of page, then look for 'Railscasts (iPod + Apple TV)' in "Also Subscribed To"
It is very helpfull
Thanks for the excellent cast, it was really useful. One minor point, it might be useful to change 'validate :recipient_is_not_registered' in Invitation to 'validate_on_create :recipient_is_not_registered'; once a user is inevitably created, it seems that this would invalidate its associated invitation. This is pretty minor considering that you don't typically need to modify invitation objects after a user has accepted, but just in case...
I get the following error when trying to send an invite (when I'm logged in):
ActionController::InvalidAuthenticityToken
Any ideas on how to fix this?
I should add I'm using Rails 2.1
Nice and Helpful, just checked the screen cast seems easy to use and efficient .
I believe the user model should be "validates_presence_of :invitation_id, :message => "is required.", :on => :create". By doing this the model will only validate that the user has an invitation on creation rather than any time the user updates anything on there profile.
great episode. I have a question that has actually come up in certain other episodes. I understand the difference between class and instance methods. But I dont understand why in the setter method of invitation_token= you set
self.invitation=Invitation.find_by_token(token)
why is it the class variable there as in self.invitation, isn't each invitation a separate instance?
Thank you!
(I am looking at around the 16:24 minute mark of this video)
I am getting nil values for :sender in the invitation model when trying to create an invite.
It seems like there's something wrong with my relationship between the user and invitation models, even though I copied your relationships in the screencast.
Anyone else having this same issue?
Please help!
thanks in advance
wasn't using rails 2.1.2! Updated rails and installed nifty_generators and now its working smooth.
Thanks!
Awesome railscast Ryan. Thanks. Used it right away on a project.
I didn't have beta invitations previously in the project, but wanted to switch to them. I ran into a problem with updating the contents of old users (since they didn't have an invitation id and since the validation still required it).
For those of you having a similar problem my solution was to add an unless statement Proc on the validation.
http://pastie.org/300700
<code>
validates_presence_of :invitation_id, :message => 'is required', :unless => Proc.new { |user| user.state == 'active' }
validates_uniqueness_of :invitation_id, :unless => Proc.new { |user| user.state == 'active' }
</code>
The code as implemented here seems to be able to handle bogus tokens (e.g., "123"), but not the absence of a token -- when I point my browser to localhost:3000/signup/ I get
Routing Error
No route matches "/signup/" with {:method=>:get}
Is there an easy way to remedy this?
Perhaps when no token is supplied, the behavior can be to go to that "add your email"-type page detailed at the end...? But more importantly, how to avoid the crash?
Thanks!
Think I figured it: the key would just be to create a second name in the routes, so you'd end up with
map.signup '/signup', :controller => 'users', :action => 'new'
map.signup_with_token '/signup/:invitation_token', :controller => 'users', :action => 'new'
Doing this allowed me to prevent my previous links to the standard signup page from throwing errors, whilst haveing the new _with_token route for the invites. Thanks for a great episode, Ryan!
adding the "join the beta" page, I noticed that when there's a validation error, it shows up back on the normal "new invitation" page. any way to do that differently?
Ok, if an invitation is required to sign up, how do you sign up an initial, "admin" user?
I ran the rake db:fixtures:load, and I looked at the rake source, but the passwords are not listed, so I have no idea what they are ;)
I tried putting an invitation token called 'test' into the invitations table, then tried http://localhost/signup/test.
I get :# invitation is required
# Invitation has already been taken
I dumped the params, and the invitation_token is blank, so it doesn't seem to be getting picked up from the browser url.
I'm running rails 2.2.2.
Any ideas ?
Mike
Actually the params ARE being passed to the ‘Users’ ‘new’ action, but apparently the authenticaton is failing somewhere.
This has been very helpful! I've made a couple tweaks to make this work for a social network, so the invited user gets auto-friended with the person who invited him, and gets pending requests from other people who may have invited him. Maybe once I get everything squared away, I'll post some of my code.
One concern I have is if the invitation's token is guaranteed to be unique. Perhaps I'm being paranoid, but I think I'm going to modify this to use the invitation's id & token to uniquely grab the invite. Thoughts?
The only link that works is the "for apple tv" link.. Thx for these screen casts
To connect to gmail, use the plugin
http://github.com/collectiveidea/action_mailer_optional_tls
It has been updated to support Ruby 1.8.6 and 1.8.7 both.
How could I combine this (would I combine this) with the "add a friend" functionality you described? I want a user to be able to invite someone via email and that invited person when joining is now a friend.
hello, GREAT tutorial.
I've got about half way though, where i need to test the invitaiton, and invite count decrements as expected. It handles missing email field, but when I enter an email address that is not listed already, I get:
undefined method `generate_token' for #<Invitation:0x2563bf8>
The invitation_controller has this:
@invitation.sender = @user
i used @user and user but bith return the same issue.
ANy ideas?
THanks
Anyone written that rake task to email the "other" beta users from the signup page?
I'm trying to use an invitation_observer.rb to send the email. How do I get the signup_url(invitation) method into the after_create method?
it would be great to do an update of this episode, so it works nicely together with Devise (see episodes 209,210).
When generating an Model through
Devise, Devise doesn't create a user_controller out of the box.. maybe you could put a note in here, on how to deal with that when trying to implement Beta-Invitations with Devise
probably something like this is needed:
# app/controllers/users_controller.rb
class UsersController < Devise::RegistrationsController
# GET /resource/sign_in
def new
super
end
# POST /resource/sign_up
def create
# add custom logic
super
end
...
end
I use Devise and have been trying to implement invitations. I am having an issues, can you help?
I can't get my devise model (user) to recognize the :invitation_token in the (new) method as pointed out in the episode, the hidden_field in the form never receives the invitation_token. It does get passed as a parameter though, I see it in logs.
Placing the :invitation_token and email assignments in the user_controller didn't work. I also created a registrations_controller and tried to override the new method. This didn't work either:
Here is my relevant routes.rb line:
And invitation_controller.rb
Any suggestions greatly appreciated, thank you.
Hi Engin,
Are you aware of devise invitable?
Hi Engin,
This might be too late for you, but I had this same issue and figured I would post the solution in case it is helpful to someone else out there.
For the Registrations controller, I had to create a "Users" folder in my controllers folder and then put the registrations_controller code below into my new "users/registrations_controller.rb" file (i.e. which inherits from the Devise registrations_controller). To understand why, take a look at the Devise registrations_controller "new" code on Github - essentially, the code that you added into your registrations_controller "new" section needs to be adjusted to use "resource" instead of "@user" and then it needs to run after the resource is built (ie. "build_resource({})"), but before the devise response code (i.e "respond_with self.resource").
For the controllers/users/registrations_controller.rb file:
For the routes.rb file:
For the invitation_controller.rb file:
Update regarding the registrations controller code I posted above - better to put the added code in "new" vs within "build resource':
class Users::RegistrationsController < Devise::RegistrationsController
def new
build_resource({})
resource.invitation_token = params[:invitation_token]
resource.email = resource.invitation.recipient_email if resource.invitation
respond_with self.resource
end
def create
super
end
end
How can i create an initial User ? If i try to create the initial User I get SQLite3::SQLException: no such column: users.invitation_id: SELECT 1 AS one FROM "users" WHERE "users"."invitation_id" IS NULL LIMIT 1 .. SO first User does not need an invitation how can i make an exception?
Nice.