- 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.
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 method
role' 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
.
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 theUser
model is instantiated. This is the right callback because if we used something else such asafter_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!