ProjectList's Models look a little like this
Conceptually, users with appropriate permissions should be able to invite other users, either existing or by email, to join an organization they are a part of. There are plenty of gems out there that take care of application-wide invitation systems, but ProjectList doesn't have any app-wide views or functions. Everything is based within the scope of an organization, so not only did these gems not work for my problem, but they weren't even a good starting point.
has_many :through
with a third model. Perhaps polymorphic associations could also be used?In addition to the following, you will need to set up a very basic mailer and add :invites to your routes
There's a lot of information to be associated with the invitation, so we need a model for it.
class Invite < ActiveRecord::Base
belongs_to :organization
belongs_to :sender, :class_name => 'User'
belongs_to :recipient, :class_name => 'User'
end
class CreateInvites < ActiveRecord::Migration
def change
create_table :invites do |t|
t.string :email
t.integer :sender_id
t.integer :recipient_id
t.string :token
t.timestamps
end
end
end
Now we have a nice way of keeping track of invitations, and if we need to add features like invitation limits or expiration time, we can do so easily.
<%= form_for @invite , :url => invites_path do |f| %>
<%= f.hidden_field :organization_id, :value => @invite.organization_id %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.submit 'Send' %>
<% end %>
When a user submits the form to make a new invite, we not only need to send the email invite, but we need to generate a token as well. The token is used in the invite URL to (more) securely identify the invite when the new user clicks to register.
before_create :generate_token
def generate_token
self.token = Digest::SHA1.hexdigest([self.organization_id, Time.now, rand].join)
end
Now, in our create
action we need to fire off an invite email (controlled by our Mailer), but ONLY if the invite saved successfully.
def create
@invite = Invite.new(invite_params) # Make a new Invite
@invite.sender_id = current_user.id # set the sender to the current user
if @invite.save
InviteMailer.new_user_invite(@invite, new_user_registration_path(:invite_token => @invite.token)).deliver #send the invite data to our mailer to deliver the email
else
# oh no, creating an new invitation failed
end
end
Here the InviteMailer takes 2 parameters, the invite and the invite URL which is constructed thusly:
new_user_registration_path(:invite_token => @invite.token)
#outputs -> http://yourapp.com/users/sign_up?invite_token=075eeb1ac0165950f9af3e523f207d0204a9efef
Registering an invited user is a little different than a brand new user
First, we need to modify our user registration controller to read the parameter from the url
def new
@token = params[:invite_token] #<-- pulls the value from the url query string
end
Next we need to modify our view to put that parameter into a hidden field that gets submitted when the user submits the registration form. I used a conditional statement within my users#new view to output this field when an :invite_token parameter is present in the url.
<% if @token != nil %>
<%= hidden_field_tag :invite_token, @token %>
<% end %>
Next we need to modify the user create
action to accept this unmapped :invite_token parameter.
def create
@newUser = build_user(user_params)
@newUser.save
@token = params[:invite_token]
if @token != nil
org = Invite.find_by_token(@token).organization #find the organization attached to the invite
@newUser.organizations.push(org) #add this user to the new organization as a member
else
# do normal registration things #
end
end
Now when the user registers, they'll automatically have access to the organization they were invited to, as expected.
Add a check to the Invite model via a before_save filter:
before_save :check_user_existence
def check_user_existence
recipient = User.find_by_email(email)
if recipient
self.recipient_id = recipient.id
end
end
This method will look for a user with the submitted email, and if found it will attach that user's ID to the invitation as the :recipient_id
Modify the Invite controller to do something different if the user already exists:
def create
@invite = Invite.new(invite_params)
@invite.sender_id = current_user.id
if @invite.save
#if the user already exists
if @invite.recipient != nil
#send a notification email
InviteMailer.existing_user_invite(@invite).deliver
#Add the user to the organization
@invite.recipient.organizations.push(@invite.organization)
else
InviteMailer.new_user_invite(@invite, new_user_registration_path(:invite_token => @invite.token)).deliver
end
else
# oh no, creating an new invitation failed
end
end
Now if the user exists, he/she wil automatically become a member of the organization.