#124
Aug 25, 2008

Beta Invitations

You know those invitation systems where a given user can invite a number of other people to join? That's what I show you how to make in this episode.
Download (39.3 MB, 22:17)
alternative download for iPod & Apple TV (28.5 MB, 22:17)

Resources

script/generate nifty_scaffold invitation sender_id:integer recipient_email:string token:string sent_at:datetime new
script/generate migration add_invitation_to_users invitation_id:integer invitation_limit:integer
script/generate mailer Mailer invitation
# models/invitation.rb
belongs_to :sender, :class_name => 'User'
has_one :recipient, :class_name => 'User'

validates_presence_of :recipient_email
validate :recipient_is_not_registered
validate :sender_has_invitations, :if => :sender

before_create :generate_token
before_create :decrement_sender_count, :if => :sender

private

def recipient_is_not_registered
  errors.add :recipient_email, 'is already registered' if User.find_by_email(recipient_email)
end

def sender_has_invitations
  unless sender.invitation_limit > 0
    errors.add_to_base 'You have reached your limit of invitations to send.'
  end
end

def generate_token
  self.token = Digest::SHA1.hexdigest([Time.now, rand].join)
end

def decrement_sender_count
  sender.decrement! :invitation_limit
end

# models/user.rb
validates_presence_of :invitation_id, :message => 'is required'
validates_uniqueness_of :invitation_id

has_many :sent_invitations, :class_name => 'Invitation', :foreign_key => 'sender_id'
belongs_to :invitation

before_create :set_invitation_limit

attr_accessible :login, :email, :name, :password, :password_confirmation, :invitation_token

def invitation_token
  invitation.token if invitation
end

def invitation_token=(token)
  self.invitation = Invitation.find_by_token(token)
end

private

def set_invitation_limit
  self.invitation_limit = 5
end

# invitation_controller.rb
def new
  @invitation = Invitation.new
end

def create
  @invitation = Invitation.new(params[:invitation])
  @invitation.sender = current_user
  if @invitation.save
    if logged_in?
      Mailer.deliver_invitation(@invitation, signup_url(@invitation.token))
      flash[:notice] = "Thank you, invitation sent."
      redirect_to projects_url
    else
      flash[:notice] = "Thank you, we will notify when we are ready."
      redirect_to root_url
    end
  else
    render :action => 'new'
  end
end

# users_controller.b
def new
  @user = User.new(:invitation_token => params[:invitation_token])
  @user.email = @user.invitation.recipient_email if @user.invitation
end

# routes.rb
map.signup '/signup/:invitation_token', :controller => 'users', :action => 'new'

# models/mailer.rb
def invitation(invitation, signup_url)
  subject    'Invitation'
  recipients invitation.recipient_email
  from       'foo@example.com'
  body       :invitation => invitation, :signup_url => signup_url
  invitation.update_attribute(:sent_at, Time.now)
end
<!-- mailer/invitation.erb -->
You are invited to join our beta!

<%= @signup_url %>

<!-- invitations/new.html.erb -->
<% form_for @invitation do |f| %>
  <p>
    <%= f.label :recipient_email, "Friend's email address" %><br />
    <%= f.text_field :recipient_email %>
  </p>
  <p><%= f.submit "Invite!" %></p>
<% end %>

<!-- home/index.html.erb -->
<p>We are currently in private beta. Please submit your email address below, and we will notify you when we are ready to accept more users.</p>

<% form_for Invitation.new do |f| %>
  <p>
    <%= f.label :recipient_email, "Your Email:" %>
    <%= f.text_field :recipient_email %>
    <%= f.submit 'Submit' %>
  </p>
<% end %>

<!-- users/new.html.erb -->
<%= f.hidden_field :invitation_token %>

See Full Source Code

RSS Feed for Episode Comments 42 comments

1. Aerpe Aug 25, 2008 at 04:44

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


2. Ryan Bates Aug 25, 2008 at 06:40

@Aerpe, that is generated by restful_authentication. I believe those are just 3 modules they use to handle the authentication.


3. Aerpe Aug 25, 2008 at 07:09

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


4. Alek Aug 25, 2008 at 09:52

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


5. Per Velschow Aug 25, 2008 at 10:36

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?


6. khelll Aug 25, 2008 at 11:27

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)


7. QuBiT Aug 25, 2008 at 11:44

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


8. Carl Aug 25, 2008 at 11:48

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


9. Carl Aug 25, 2008 at 11:49

QuBiT beat me to it!


10. Ryan Bates Aug 25, 2008 at 12:31

@Per Velschow, hmm, it's working for me. Going to pragmatic.tv should redirect you to:
http://www.pragprog.com/screencasts

@khelll, thanks fixed.


11. Alek Aug 25, 2008 at 12:42

@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


12. Pierre Aug 25, 2008 at 13:08

Thanks Ryan for this great episode.
I love this podcast so much !


13. mpearce Aug 25, 2008 at 13:17

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.


14. Alek Aug 25, 2008 at 14:06

@mpearce, Thank you! This is a terrific help, and to rest of you guys! Thanks!
A


15. Ian Aug 25, 2008 at 17:10

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


16. matt Aug 26, 2008 at 08:00

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!


17. James Aug 26, 2008 at 10:01

Great screencast, Ryan. Now you left yourself to show us how to write the rake task that sends out the next 100 invites :)


18. Philip (flip) Kromer Aug 26, 2008 at 10:14

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.


19. Michael Aug 26, 2008 at 18:00

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.


20. Michael Aug 26, 2008 at 18:07

I should add I'm using Rails 2.1


21. Ryan Bates Aug 26, 2008 at 18:38

@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?


22. Michael Aug 26, 2008 at 22:33

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


23. matt Aug 27, 2008 at 07:05

Of course not. That would probably solve my problem. :-)


24. Ryan Bates Aug 27, 2008 at 07:50

@Michael, hmm, I'd have to see the code. Could you email me the user.rb file? ryan AT railscasts DOT com.


25. Greg Aug 29, 2008 at 16:20

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.


26. Philip (flip) Kromer Sep 01, 2008 at 23:41

@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"


27. venkat Sep 06, 2008 at 21:52

It is very helpfull


28. Liam Morley Sep 15, 2008 at 20:01

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


29. Mark Sep 30, 2008 at 08:39

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?


30. Ferienwohnung Sizilien Oct 06, 2008 at 11:19

I should add I'm using Rails 2.1


31. Piyush Oct 09, 2008 at 22:15

Nice and Helpful, just checked the screen cast seems easy to use and efficient .


32. Jacob Oct 10, 2008 at 11:49

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.


33. zachary denison Oct 17, 2008 at 13:58

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)


34. Mike Oct 22, 2008 at 22:40

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


35. mike Oct 25, 2008 at 17:13

wasn't using rails 2.1.2! Updated rails and installed nifty_generators and now its working smooth.

Thanks!


36. Scott Motte Oct 26, 2008 at 00:48

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>


37. cheap wow gold Oct 29, 2008 at 02:59

Thank you for useful information.


38. fiesta money Nov 20, 2008 at 18:41

Thanks Ryan,I think this is one of the most wonderful sites. I have great admiration for you.


39. maple story mesos Nov 24, 2008 at 23:25

I have great admiration for you.


40. lily Nov 27, 2008 at 19:41

Thank you Ryan, your screencast is good. Please look at our URL, if necessary we can learn from each other.


41. brian Dec 02, 2008 at 15:57

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!

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player