Customizing Devise
Walk through how to customize the Devise gem, including adding new parameters and setting default values.
Guide Tasks
  • Read Tutorial

Now that we have Devise installed and working properly, we need to implement some custom behavior in order to satisfy some of our functional requirements. Let's review the requirements once again:

  • user should be required to have a first name, last name, role, and username
  • user role attribute should default to ‘student’ when created
  • user should be able to register
  • user should be able to sign in
  • user should be able to delete her account
  • user should be able to edit her account details
  • user should not be able to edit her role
  • user username should be a subdomain
  • user username should be unique
  • user username should not allow for special characters

user should be required to have a first name, last name, role, and username

We already added the first_name, last_name, role, and username columns to the users table, however we need to add in form fields for the new parameters. Also note, since these are new parameters that Devise doesn't have built in, we'll need to add them to the list of strong parameters so Rails knows that they're valid.

Let's start out by writing some specs to test this behavior, if I'm not using any custom fields I typically won't test default behavior with Devise since they test it all anyways, however now we're implementing some custom behavior and that needs to be tested. Create a new file user_spec.rb

# spec/features/user_spec.rb

require 'rails_helper'

describe 'user navigation' do
  describe 'creation' do
    it 'can register with full set of user attributes' do
      visit new_user_registration_path

      fill_in 'user[email]', with: "test@test.com"
      fill_in 'user[password]', with: "password"
      fill_in 'user[password_confirmation]', with: "password"
      fill_in 'user[first_name]', with: "Jon"
      fill_in 'user[last_name]', with: "Snow"
      fill_in 'user[username]', with: "watcheronthewall"

      click_on "Sign up"

      expect(page).to have_content("Welcome")
    end
  end
end

Running rspec spec/features/user_spec.rb will give the the following error Capybara::ElementNotFound: Unable to find field "user[first_name]". This makes sense since we haven't added the field yet, let's update the registration template with our new fields:

<!-- app/views/devise/registrations/new.html.erb -->

<h2>Sign up</h2>

<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
  <%= devise_error_messages! %>

  <div class="field">
    <%= f.label :email %><br />
    <%= f.email_field :email, autofocus: true %>
  </div>

  <div class="field">
    <%= f.label :username %><br />
    <%= f.text_field :username %>
  </div>

  <div class="field">
    <%= f.label :first_name %><br />
    <%= f.text_field :first_name %>
  </div>

  <div class="field">
    <%= f.label :last_name %><br />
    <%= f.text_field :last_name %>
  </div>

  <div class="field">
    <%= f.label :password %>
    <% if @minimum_password_length %>
    <em>(<%= @minimum_password_length %> characters minimum)</em>
    <% end %><br />
    <%= f.password_field :password, autocomplete: "off" %>
  </div>

  <div class="field">
    <%= f.label :password_confirmation %><br />
    <%= f.password_field :password_confirmation, autocomplete: "off" %>
  </div>

  <div class="actions">
    <%= f.submit "Sign up" %>
  </div>
<% end %>

<%= render "devise/shared/links" %>

Now, let's add in the new attributes to the list of strong parameters. Create a new controller registrations_controller.rb:

# app/controllers/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController

  private

  def sign_up_params
    params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :username)
  end

  def account_update_params
    params.require(:user).permit(:first_name, :last_name, :email, :password, :password_confirmation, :current_password, :username)
  end
end

This extends the Devise controller and adds in the list of attributes that we're going to use in our registration forms. Lastly, we need to customize the route for registrations:

# config/routes.rb

Rails.application.routes.draw do
  devise_for :users, controllers: { registrations: 'registrations' }
  root to: 'static#home'
end

Running the spec again and we see it's all working, nice work! You can startup the Rails server and navigating to localhost:3000/users/sign_up you'll see our new fields and that they're all working properly.

medium


user role attribute should default to ‘student’ when created

We don't want users to have the ability to customize their role, so this will be behavior that has to happen automatically. Let's create a model spec to build this feature. By running our User model generator RSpec already created a model spec file for us, so let's open it up and add a few specs:

# spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'creation' do
    before do
      @user = User.create(email: "test@test.com", password: "password", password_confirmation: "password", first_name: "Jon", last_name: "Snow", username: "watcheronthewall")
    end

    it 'should be able to be created if valid' do
      expect(@user).to be_valid
    end

    it 'should have a default role of: student' do
      expect(@user.role).to eq('student')
    end
  end
end

This has two basic specs, one of them should already pass since we know that users can be created, the second spec should fail since it's checking to see if a role is set by default. Running rspec spec/models gives us the following error: NoMethodError: undefined methodrole' for #User:0x007fd2b09991e0`. Let's create a migration to fix this error:

rails g migration add_role_to_users role:string

After running rake db:migrate it will add the role attribute to the User model and we can run the spec again. Now we're getting a different error, RSpec says that it expected: "student", got: nil.

medium

Let's get this test passing by adding in a callback in the user.rb model file so users start off with a default role when created:

# app/models/user.rb

class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  after_initialize :set_defaults

  private

    def set_defaults
      self.role ||= 'student'
    end
end

This is a rather clever way to set defaults. You could set defaults using a database migration, but that feels messy to me and it's difficult to change later on, so let's walk through what this is doing:

  • after_initialize is a callback that occurs when the User model is instantiated. This is the right callback because if we used something else such as after_create it would override any other value that we may have integrated on the fly.

  • set_defaults is a method where we can list any default values that we may want to have.

  • self.role ||= 'student' says that if the role attribute is nil, set it to student

Running the specs now will show that everything is working.


Now that we have a model spec, let's revisit the user should be required to have a first name, last name, role, and username requirement. We need to integrate some validations to ensure that a user cannot be created without having these parameters. Let's add in a describe block to our user model to test these validations:

# spec/models/user_spec.rb

require 'rails_helper'

RSpec.describe User, type: :model do
  describe 'creation' do
    before do
      @user = User.create(email: "test@test.com", password: "password", password_confirmation: "password", first_name: "Jon", last_name: "Snow", username: "watcheronthewall")
    end

    it 'should be able to be created if valid' do
      expect(@user).to be_valid
    end

    it 'should have a default role of: student' do
      expect(@user.role).to eq('student')
    end

    describe 'validations' do
      it 'should not be valid without a first name' do
        @user.first_name = nil
        expect(@user).to_not be_valid
      end

      it 'should not be valid without a last name' do
        @user.last_name = nil
        expect(@user).to_not be_valid
      end

      it 'should not be valid without a username' do
        @user.username = nil
        expect(@user).to_not be_valid
      end
    end
  end
end

Running rspec spec/models will give three failures, saying Failure/Error: expect(@user).to_not be_valid. Let's get this passing by updating our User model with validations:

# app/models/user.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  after_initialize :set_defaults

  validates_presence_of :first_name, :last_name, :username

  private

    def set_defaults
      self.role ||= 'student'
    end
end

Run the specs again and you'll see that everything is working, nice work!


Let's see where we are with the requirements:

  • user should be required to have a first name, last name, role, and username - done
  • user role attribute should default to ‘student’ when created - done
  • user should be able to register - done automatically with devise
  • user should be able to sign in - done automatically with devise
  • user should be able to delete her account - done automatically with devise
  • user should be able to edit her account details - done automatically with devise
  • user should not be able to edit her role - done
  • user username should be a subdomain - will implement later
  • user username should be unique
  • user username should not allow for special characters

Ok, we're almost there, let's tackle these next two.

user username should be unique

Let's add in a validation to our model spec to verify that a username is always going to be unique (for the sake of space I'm going to nest this in the validation block, but I won't print it all out):

# spec/models/user_spec.rb

it 'should ensure that all usernames are unique' do
  duplicate_username_user = User.create(email: "test2@test.com", password: "password", password_confirmation: "password", first_name: "Joni", last_name: "Snowy", username: "watcheronthewall")
  expect(duplicate_username_user).to_not be_valid
end

Running the specs will give us the following error Failure/Error: expect(duplicate_username_user).to_not be_valid. Let's implement this feature by updating the model file with the code below:

# app/models/user.rb

validates_uniqueness_of :username

If you run the specs now you'll see that we're back to green and the feature has been successfully implemented.


Ok, we're onto our last feature of the lesson. user username should not allow for special characters

Since our usernames are eventually going to be subdomains, we need to ensure that no special characters, such as &*$&*@, etc. will be used since that could cause a number of issues. Let's add this spec to the model tests:

# spec/models/user_spec.rb

it 'should ensure that usernames do not allow special characters' do
  @user.username = "*#*(@!"
  expect(@user).to_not be_valid
end

As expected this will give us an error, let's refactor our model file to ensure that only regular characters are used for the usernames:

# app/models/user.rb

class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  after_initialize :set_defaults

  validates_presence_of :first_name, :last_name
  validates :username, uniqueness: true, presence: true, format: { with: /\A[a-zA-Z]+([a-zA-Z]|\d)*\Z/ }

  private

    def set_defaults
      self.role ||= 'student'
    end
end

Notice how we've consolidated all of the validations for the username attribute to a single line? It's validating: uniqueness, presence, and the format. Now if you run the specs you'll see that they're all passing and we've knocked out quite a few of the User functional requirements, very nice work!

Resources