13 Apr 2015
With Gmail, there are clear sending limitations.
Sending out emails is something that you should offload onto a third-party provider as it is a critical business process. You want to ensure emails are recieved right away and not in the recipients email folder.
Mailgun & Postmark are the recommended email service providers. When considering vendors, you want to focus on devliverability.
Check out the is Quora article on comparing well-known email service providers. In this course, we will use Mailgun.
Good blogpost for getting set up with Gmail.
07 Apr 2015
In our Todos example, we created a method
before_create :generate_token
for and then defined the generate_ token method under private,
within the Todos model.
But what if you want to include this in several models?
DRY does not only mean "Dont Repeat Yourself", but it also means to have
one authoritative source for method calls
That's where ActiveSupport::Concerns can help by basically extracting the common code into a module
lib/tokenable.rb
module Tokenable
extend ActiveSupport::Concern
included do
before_create :generate_token
end
def generate_token
self.token = SecureRandom.urlsafe_base64
end
end
Then in the Todo model:
class Todo < ActiveRecord::Base
include Tokenable
With Rails 3 or later, the lib directory is no longer automatically loaded
You instead have to:
require_relative '../..lib/tokenable'
class Todo < ActiveRecord::Base
include Tokenable
end
Now if you dont want to require_relative everytime:
You can do one of the two:
- move the Tokenable file to your models folder
- go to /enviornments/application.rb
config.autoload_paths << "#{Rails.root}/lib"
then remove require_relative from the model.
05 Apr 2015
Feature Spec: Inviting Users
require 'spec_helper'
feature "User invited friend" do
scenario "User succesfully intvites friend and invitation is accepted" do
jam = Fabricate(:user)
sign_in(jam)
invite_a_friend
friend_accepts_invitation
friend_signs_in
friend_should_follow(jam)
inviter_should_follow_friend(jam)
clear_email
end
def invite_a_friend
visit new_invitation_path
fill_in "Friend's Name", with: 'Amber Howard'
fill_in "Friend's Email Address", with: 'amberhoward1@gmail.com'
fill_in "Message", with: "Hey girl, you gotta check this out!"
click_button "Send Invitation"
sign_out
end
def friend_accepts_invitation
open_email "amberhoward1@gmail.com"
current_email.click_link "Accept this invitation"
fill_in "Password", with: "password"
fill_in "Full Name", with: "Amber Howard"
click_button "Sign Up"
end
def friend_signs_in
fill_in "Email Address", with: 'amberhoward1@gmail.com'
fill_in "Password", with: 'password'
click_button "Sign in"
end
def friend_should_follow(inviter)
click_link "People"
expect(page).to have_content inviter.full_name
sign_out
end
def inviter_should_follow_friend(inviter)
sign_in(inviter)
click_link "People"
expect(page).to have_content "Amber Howard"
end
end
03 Apr 2015
Inviting 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
31 Mar 2015
Let's write add functionality to allow users to invite their friends:
Workflow:
- User can input friends name and email, click invite friend link
- An email is sent to friend
- Friend open email, clicks link and then is directed to Registration Form
- Email address is pre-filled
- Upon sign up, user automatically follows recommender and vice versa
Steps:
1. Copy ui controller into views/invitations/new.html.haml
%section.invite.container
.row
.col-sm-10.col-sm-offset-1
%form.invite
%header
%h1 Invite a friend to join MyFlix!
%fieldset
.form-group
%label Friend's Name
.row
.col-sm-4
%input.form-control(type="text")
.form-group
%label Friend's Email Address
.row
.col-sm-4
%input.form-control(type="email")
.form-group
%label Invitation Message
.row
.col-sm-6
%textarea.form-control(rows=6) Please join this really cool site!
%fieldset.form-group.action
%input(type="submit" value="Send Invitation" class="btn btn-default")
2. Edit template to create need attributes: recipient name, email, message
section.invite.container
.row
.col-sm-10.col-sm-offset-1
%form.invite
= bootstrap_form_for @invitation, class: "invite" do |f|
%header
%h1 Invite a friend to join MyFlix!
%fieldset
= f.text_field :recipient_name, label: "Friend's Name"
= f.text_field :recipient_email, label: "Friend's Email Address"
= f.text_area :message, class: "span4", rows: 6, placeholder: "Please join this really cool site!", label: "Message"
%fieldset.form-group.action
%input(type="submit" value="Send Invitation" class="btn btn-default")
3. Create new and create routes for invitations
resources :invitations, only: [:new, :create]
4. Create InvitationsController
5. Begin with InvitationsController spec
6. Create context: for valid inputs and invalid inputs
GET new
#invitation_controller_spec.rb
require 'spec_helper'
describe InvitationsController do
describe "GET new" do
it "sets @invitation to a new invitation" do
set_current_user
get :new
expect(assigns(:invitation)).to be_new_record
expect(assigns(:invitation)).to be_instance_of Invitation
end
it_behaves_like "requires sign in" do
let(:action) { get :new }
end
end
class InvitationsController < ApplicationController
before_filter :require_user
def new
@invitation = Invitation.new
end
Generate Migration
class CreateInvitations < ActiveRecord::Migration
def change
create_table :invitations do |t|
t.integer :inviter_id
t.string :recipient_name, :recipient_email
t.text :message
t.timestamps
end
end
end
Create Invitation Model/Model Spec
#invitation.rb
class Invitation < ActiveRecord::Base
before_create :generate_token
belongs_to :inviter, class_name: "User"
validates_presence_of :recipient_name, :recipient_email, :message
def generate_token
self.token = SecureRandom.urlsafe_base64
end
end
#invitation_model_spec.rb
require 'spec_helper'
describe Invitation do
it { should validate_presence_of(:recipient_name) }
it { should validate_presence_of(:recipient_email) }
it { should validate_presence_of(:message) }
POST create
Context: with valid Inputs
#invitations_controller_spec.rb
describe "POST create" do
it_behaves_like "requires sign in" do
let(:action) { post :create }
end
context "with valid input" do
it "redirects to ithe invitation new page" do
set_current_user
post :create, invitation: { recipient_name: "Jam Black", recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(response).to redirect_to new_invitation_path
end
it "creates the invitation" do
set_current_user
post :create, invitation: { recipient_name: "Jam Black", recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(Invitation.count).to eq(1)
end
it "sends an email to the recipient" do
set_current_user
post :create, invitation: { recipient_name: "Jam Black", recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(ActionMailer::Base.deliveries.last.to).to eq(['jam@jamblack.com'])
end
it "sets the flash success message" do
set_current_user
post :create, invitation: { recipient_name: "Jam Black", recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(flash[:success]).to be_present
end
end
Context: With invalid inputs
#invitations_controller_spec.rb
context "with invalid input" do
after { ActionMailer::Base.deliveries.clear }
it "renders the :new template" do
set_current_user
post :create, invitation: { recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(response).to render_template :new
end
it "does not create an invitation" do
set_current_user
post :create, invitation: { recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(Invitation.count).to eq(0)
end
it "does not send out an email" do
set_current_user
post :create, invitation: { recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(ActionMailer::Base.deliveries).to be_empty
end
it "sets the flash error message" do
set_current_user
post :create, invitation: { recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(flash[:error]). to be_present
end
it "sets #@invitation" do
set_current_user
post :create, invitation: { recipient_email: "jam@jamblack.com", message: "Join Myflix!" }
expect(assigns(:invitation)).to be_present
end
end
end
end
#invitations_controller.rb
def create
@invitation = Invitation.create(invitation_params.merge!(inviter_id: current_user.id))
if @invitation.save
AppMailer.send_invitation_email(@invitation).deliver
flash[:success] = "You've successfully invited #{@invitation.recipient_name}."
redirect_to new_invitation_path
else
flash[:error] = "Please fill in all inputs."
render :new
end
end
private
def invitation_params
params.require(:invitation).permit(:recipient_name, :recipient_email, :message, :inviter_id)
end
end
class AppMailer < ActionMailer::Base
def send_invitation_email(invitation)
@invitation = invitation
mail to: invitation.recipient_email, from: "info@myflix.com", subject: "Invitation to join Myflix"
end
end
Remember to:
clear the ActionMailer deliveries in the valid input context so they don't interfer with
the invalid inputs. - we want that number to be 0