Jun 15, 2016
Table of contents:
A couple of weeks ago I built out the action for creating new users in Culttt using a Trailblazer Operation (Using Inheritance for Trailblazer Operations).
In this application I need 3 different ways of creating new users. Firstly, I need to import users from the old WordPress application. Secondly, I need to create confirmed users to seed the database. And finally, I need the default Operation that will be used as part of the registration process.
When a user registers with the application using the default Operation, I will send a confirmation email that will allow the user to confirm their email address.
But I also need a way to allow imported users to confirm their email address, as well as choose a username, and a password.
In today’s tutorial we’re going to be looking at adding the functionality to request a confirmation email. This will touch upon a couple of interesting aspects of Trailblazer Operations that we haven’t covered so far, and it will hopefully reinforce how Trailblazer fits inside of a typical Rails application.
So with that being said, lets get started!
Before we jump into the code, lets have a quick review of what we’re going to build.
We’re first going to need two new routes. One for displaying a form that accepts an email to send the confirmation email to, and one to accept the POST
request from the form.
We will need a Controller to accept those requests and we need to create the UserMailer
class, and email view template.
We will need to update the User::Create::Operation::Default
class to send a confirmation automatically after saving the new user and we will need to create a new Operation for requesting a confirmation email.
We will need to create a new Cell class and view template for displaying the form, and of course we will need a bucket load of tests to make sure everything is working as it should.
Phew! We best get started then!
The first thing we need to do is to add two new routes to the routes.rb
under the config
directory:
Rails
.application
.routes
.draw do
# Join
get 'join', to: 'join#new'
post 'join', to: 'join#create'
# Login
get 'login', to: 'login#new'
# Confirmation
get 'confirmation/request', to: 'confirmation/request#new'
post 'confirmation/request', to: 'confirmation/request#create'
end
If you are familiar with routing in Rails (Defining URL routes in Ruby on Rails), this should be fairly straight forward for you.
There is going to be a request action and a confirm action for this confirmation process.
As you might have noticed, I’m going to separate these two actions into two separate controllers under the Confirmation
namespace.
I would rather keep this as two small Controllers, rather than one bigger Controller.
Next up I will add the Controller to handle the requests:
module Confirmation
class RequestController < ApplicationController
def new
form Confirmation::Request::Operation
end
def create
run Confirmation::Request::Operation do |op|
return redirect_to login_url
end
render :new, status: 400
end
end
end
There isn’t a great deal going on in this Controller, which is a lovely side-effect of using Trailblazer. If you have been following along with these Trailblazer tutorials this is going to look very familiar.
I’ve added the Confirmation::Request::Operation
even though I haven’t created it yet because I know exactly what this Controller is going to look like when all the pieces are in place.
At this point I will also add the view form the #new
method:
h1 Confirm your membership = concept("confirmation/request/cell", @operation)
Due to the fact that we’re using a Cell to encapsulate this view, it is very simple.
Before we start creating the Operation class, first we need to generate a new UserMailer
class using Rails’ generate command:
bin/rails generate mailer UserMailer
This should create a new user_mailer.rb
file under the mailers
directory. In this new class I will add a method for confirming a membership:
class UserMailer < ApplicationMailer
def confirm_membership(user)
@user = user
@url = # generate confirmation url
mail(to: @user.email, subject: 'Confirm your Culttt Membership!')
end
end
If you are new to Action Mailer, take a look at Getting started with Action Mailer in Ruby on Rails.
I haven’t set the @url
variable yet as we don’t have the confirmation routes. I’ll come back to this when we implement this functionality.
With the User Mailer class generated and ready to go we can update the User::Create::Operation::Default
class to automatically send a confirmation email when a new user is saved:
class Default < Base
contract User::Create::Contract::Default
def process(params)
validate(params[:user]) do |f|
generate_digest
generate_token
f.save
send_confirmtion_email
end
end
def send_confirmtion_email
UserMailer.confirm_membership(model).deliver_later
end
end
I will also update the test to ensure that the email is sent when a new user is created with this Operation:
test 'create default user' do
res, op = User::Create::Operation::Default.run(user: attributes_for(:user))
mail = ActionMailer::Base.deliveries.last
assert(res)
assert_not(op.model.confirmed?)
assert_not(op.model.imported?)
assert_equal(op.model.email, mail[:to].to_s)
assert_equal('Confirm your Culttt Membership!', mail[:subject].to_s)
end
Here I’m getting the last email that was sent via Action Mailer and then asserting that the email and subject line are correct.
Next up we need to build out the Operation that will accept the POST
request from the user when a confirmation email is requested.
The POST
request will contain an email address that should be a registered, but unconfirmed email address that exists in the database.
This Operation is a bit different to the Operations we’ve seen in previous tutorials because we are not creating or updating a model:
module Confirmation::Request
class Operation < Trailblazer::Operation
contract do
undef persisted?
attr_reader :user
property :email, virtual: true
validate :email_is_unconfirmed
validates :email, presence: true, email: true
def email_is_unconfirmed
@user = User.find_by(email: email, confirmed_at: nil)
errors.add(:email, :not_found) unless @user
end
end
def process(params)
validate(params[:user]) do |f|
UserMailer.confirm_membership(contract.user).deliver_later
end
end
end
end
As you can see from this Operation, it’s a bit different from what we’ve seen so far. In this Operation, we need to find the user by their email address, but only if the user is not confirmed.
If the user is found, we can use the Mailer class from earlier to dispatch the confirmation email.
Here are the tests for this Operation:
require 'test_helper'
module Confirmation::Request::OperationTest
class RequestCase < ActiveSupport::TestCase
test 'require email' do
res, op = Confirmation::Request::Operation.run(user: {})
assert_not(res)
assert_includes(op.errors[:email], "can't be blank")
end
test 'require valid email' do
res, op =
Confirmation::Request::Operation.run(user: { email: 'invalid email' })
assert_not(res)
assert_includes(op.errors[:email], 'is invalid')
end
test 'require unconfirmed email' do
res, op =
Confirmation::Request::Operation.run(user: { email: 'name@domain.com' })
assert_not(res)
assert_includes(op.errors[:email], 'not found')
end
test 'ignore confirmed users' do
@user =
User::Create::Operation::Confirmed.(user: attributes_for(:user)).model
res, op =
Confirmation::Request::Operation.run(user: { email: @user.email })
assert_not(res)
assert_includes(op.errors[:email], 'not found')
end
test 'send confirmation email' do
@user =
User::Create::Operation::Default.(user: attributes_for(:user)).model
res, op =
Confirmation::Request::Operation.run(user: { email: @user.email })
mail = ActionMailer::Base.deliveries.last
assert(res)
assert_equal(@user.email, mail[:to].to_s)
assert_equal('Confirm your Culttt Membership!', mail[:subject].to_s)
end
end
end
As you can see, these tests are pretty similar to the tests we’ve written in previous weeks.
First I write a test for each business rule that I want to enforce, and then finally I write a test to confirm the happy path.
As with last week’s tutorial (Getting started with Trailblazer Cells) I’m going to be encapsulating the form for this functionality in a Cell:
module Confirmation::Request
class Cell < Culttt::Cells::Form
def show
render
end
end
end
Once again I’m extending Culttt::Cells::Form
from last week’s tutorial. This is also a really basic example of a Cell class, and so there isn’t any additional logic that is required in the class itself.
I will also need the template file:
- if form.errors.any? ul - form.errors.full_messages.each do |msg| li = msg =
form_tag confirmation_request_url, class: "confirmation-request-form" do |f| div
= label_tag :email, nil, class: "qa-email-label" = text_field_tag :email, nil,
class: "qa-email-input" div = submit_tag "Request Confirmation", class:
"qa-submit"
This is your typical Rails form using the form_tag
method, rather than the usual form_for
method. As we’re using a Cell to render this form, this file lives under the concept directory.
I will also include some simple assertions to ensure that all of the correct form fields are present in the Cell template:
require 'test_helper'
module Confirmation::Request::CellTest
class CellTest < Cell::TestCase
controller Confirmation::RequestController
test 'has correct markup' do
html =
concept(
'confirmation/request/cell',
Confirmation::Request::Operation.present({})
).()
html.must_have_selector('form.confirmation-request-form')
html.must_have_selector('label.qa-email-label')
html.must_have_selector('input.qa-email-input')
html.must_have_selector('input.qa-submit')
end
end
end
If you missed the setup to this kind of testing from Getting started with Trailblazer Cells, I would recommend that you go back and take a look. I really love how easy it is to test Cells in isolation.
With all of the functionality in place, we can add some Controller tests to set through the process and ensure everything is working as it should:
require 'test_helper'
module Confirmation
class RequestControllerTest < ActionController::TestCase
test 'display form' do
get :new
assert_response(:success)
end
test 'fail with invalid email' do
post :create, user: { email: 'invalid email' }
assert_response(400)
end
test 'fail with not found email' do
post :create, user: { email: 'name@domain.com' }
assert_response(400)
end
test 'fail with confirmed email' do
@user =
User::Create::Operation::Confirmed.(user: attributes_for(:user)).model
post :create, user: { email: @user.email }
assert_response(400)
end
test 'send confirmation email' do
@user =
User::Create::Operation::Default.(user: attributes_for(:user)).model
assert_difference 'ActionMailer::Base.deliveries.size' do
post :create, user: { email: @user.email }
end
assert_response(302)
assert_redirected_to(login_url)
end
end
end
Hopefully this reads well and you’re familiar with what we’re testing. I’m basically just walking through the business rules and checking that they fail. I don’t check the actual errors for each case because I’m already check them at the Unit level.
In the final test I’m asserting that the request was successful and that the email was sent correctly.
Hopefully today’s tutorial was beneficial in that it was an example of where we’re using an Operation to perform a process that is not directly creating or updating a 1-to-1 resource.
Despite this we have still used many of the same techniques that we’ve been building up over the last couple of weeks.
Building out this functionality can often be boring. This kind of boiler plate code is basically the same for just about every type of web application.
But hopefully you can see how nice and simple Trailblazer makes it so you don’t have to worry about too much complexity.