Build Robust & Production Quality Applications - Lesson 6: Inviting Users- Part 2
03 Apr 2015Inviting Users - 2nd part of Workflow:
- User gets email and selects the "Accept this invitation" link. URL will contain special token
- Registation form has email prefilled
- Upon sign up, user automatically follows recommender and vice versa
Step 1. User recieves email:
#views/app_mailers/send_invitation_email.html.haml
!!! 5
%html(lang="en-US")
%body
%p You are inivited by #{@invitation.inviter.full_name} to join Myflix!
%p= @invitation.message
%p= link_to "Accept this invitation", register_with_token_url(@invitation.token)
Create routes
resources :users, only: [:create, :show]
get 'register/:token', to: "users#new_with_invitation_token", as: 'register_with_token'
#users_controller_spec.rb
describe "GET new_with_invitation_token" do
it "render the :new view template" do
invitation = Fabricate(:invitation)
get :new_with_invitation_token, token: invitation.token
expect(response).to render_template :new
end
it "sets @user with recipient's email" do
invitation = Fabricate(:invitation)
get :new_with_invitation_token, token: invitation.token
expect(assigns(:user).email).to eq(invitation.recipient_email)
end
it "sets @invitation_token" do
invitation = Fabricate(:invitation)
get :new_with_invitation_token, token: invitation.token
expect(assigns(:invitation_token)).to eq(invitation.token)
end
it "redirects to expired token page for invalid tokens" do
get :new_with_invitation_token, token: 'asdffdhh'
expect(response).to redirect_to expired_token_path
end
We need to fabricate an invitation:
#fabricators/invitation_fabricators.rb
Fabricator(:invitation) do
recipient_name { Faker::Name.name }
recipient_email { Faker::Internet.email }
message { Faker::Lorem.paragraphs(2).join(" ") }
end
Add Tokens Column to Invitations:
class AddTokenToInvitations < ActiveRecord::Migration
def change
add_column :invitations, :token, :string
end
end
#users_controller.rb
def new_with_invitation_token
invitation = Invitation.where(token: params[:token]).first
if invitation
@user = User.new(email: invitation.recipient_email)
@invitation_token = invitation.token
render :new
else
redirect_to expired_token_path
end
end
private
def user_params
params.require(:user).permit!
end
def handle_invitations
if params[:invitation_token].present?
invitation = Invitation.where(token: params[:invitation_token]).first
@user.follow(invitation.inviter)
invitation.inviter.follow(@user)
invitation.update_column(:token, nil)
end
end
end
User registers upon clicking link
#users/new.html.haml
%section.register.container
.row
.col-sm-10.col-sm-offset-1
= bootstrap_form_for @user, html: { class: "form-horizontal"} do |f|
%header
%h1 Register
%fieldset
.col-sm-6
= f.email_field :email, label: "Email Address"
= f.password_field :password
= f.text_field :full_name, label: "Full Name"
= hidden_field_tag :invitation_token, @invitation_token
%fieldset.actions.control-group.col-sm-offset-0
.controls
%input(type="submit" value="Sign Up" class="btn btn-default")
Gotchas
Another issue with the current form is when the invitee signs up, we lose the lose inviter's info and they are not not following the inviter. To fix this, we need a hidden field on the invitee registration form. This hidden field is a field tag, rather than model backed becasue we don't want this token to be stored under the user's token.
Step 3: Inviter/Invitee automatically follow each other after being invited:
#users_controller_spec.rb
describe "POST create" do
context "with valid input" do
before { post :create, user: Fabricate.attributes_for(:user) }
it "creates the user" do
expect(User.count).to eq(1)
end
it "redirects to th sign on page" do
expect(response).to redirect_to sign_in_path
end
it "makes the user follow the inviter" do
alice = Fabricate(:user)
invitation = Fabricate(:invitation, inviter: alice, recipient_email: 'jam@jamblack.com')
post :create, user: {email: 'jam@jamblack.com', password: 'password', full_name: 'Jam Black'}, invitation_token: invitation.token
jam = User.where(email: 'jam@jamblack.com').first
expect(jam.follows?(alice)).to be_true
end
it "makes the inviter follow the user" do
alice = Fabricate(:user)
invitation = Fabricate(:invitation, inviter: alice, recipient_email: 'jam@jamblack.com')
post :create, user: {email: 'jam@jamblack.com', password: 'password', full_name: 'Jam Black'}, invitation_token: invitation.token
jam = User.where(email: 'jam@jamblack.com').first
expect(alice.follows?(jam)).to be_true
end
it "expires the invitation upon acceptance" do
alice = Fabricate(:user)
invitation = Fabricate(:invitation, inviter: alice, recipient_email: 'jam@jamblack.com')
post :create, user: {email: 'jam@jamblack.com', password: 'password', full_name: 'Jam Black'}, invitation_token: invitation.token
jam = User.where(email: 'jam@jamblack.com').first
expect(Invitation.first.token).to be_nil
end
end
describe "#follows?" do
it "returns true if the user has a following relationship with another user" do
alice = Fabricate(:user)
bob = Fabricate(:user)
Fabricate(:relationship, leader: bob, follower: alice)
expect(alice.follows?(bob)).to be_true
end
it "returns false if the user does not have a following relationship with another user" do
alice = Fabricate(:user)
bob = Fabricate(:user)
Fabricate(:relationship, leader: alice, follower: bob)
expect(alice.follows?(bob)).to be_false
end
end
describe "#follow" do
it "follows another user" do
alice = Fabricate(:user)
bob = Fabricate(:user)
alice.follow(bob)
expect(alice.follows?(bob)).to be_true
end
it "does not follow one self" do
alice = Fabricate(:user)
alice.follow(alice)
expect(alice.follows?(alice)).to be_false
end
end
end
#users.rb
def follows?(another_user)
following_relationships.map(&:leader).include?(another_user)
end
def can_follow?(another_user)
!(self.follows?(another_user) || self == another_user)
end
def follow(another_user)
following_relationships.create(leader: another_user) if can_follow?(another_user)
end
end