- Read Tutorial
In this guide we will implement the functionality to dynamically update the view based on if a user is logged in or not. For small applications and tutorials it's pretty common to accomplish this type of behavior by leveraging the current_user
method provided by the Devise gem, and that's fine for beginner applications (I even used it in the Learn Ruby on Rails from Scratch course), however this implementation has a few issues. First let's see what this could look like in our app:
<!-- app/views/shared/_header.html.erb --> <% if current_user %> <strong>Welcome <%= current_user.full_name %></strong> <%= link_to "Logout", destroy_user_session_path, method: :delete %> <% else %> <strong>Welcome Guest!</strong> <%= link_to "Login", new_user_session_path %> or <%= link_to "Register", new_user_registration_path %> <% end %>
This isn't horrible, however we have some duplicate code and more importantly it's not a very confident way to code. We're essentially saying:
We're not sure if a user is going to be signed in or not, so we need to check it in the view so we don't get a
nil
error.
It's a bad practice to check for nil values in the view. A better implementation would be to override the current_user
method so that we're 100% sure that it won't return nil
, instead we'll have it return a Guest
object. For applications that utilize a robust permission structure we may even want to implement a GuestUser
class, however for our app we can create an OpenStruct
that will have all of the necessary parameters that a user
would have so that even if we run code such as: current_user.full_name
, when a user isn't logged in it will output something like Guest
.
Let's start off by creating some specs that check for this behavior. Inside of the static_spec
we'll create a new describe
block that will test the header
partial. Technically you could put this in a view test, however since this is going to combine implementation changes to multiple parts of the application I like designating it as an integration spec (I'm also note a huge fan of view tests since they seem to break too easily, especially at this stage of the application).
# spec/features/static_spec.rb describe 'header' do it 'has a header that displays the users name' do user = FactoryGirl.create(:user) login_as(user, :scope => :user) visit root_path expect(page).to have_content("Jon Snow") end end
This fails and let's us know that it couldn't find the user's name:
Let's implement the most basic implementation to get this working, we'll simply paste in the view code referenced at the beginning of this guide:
If you run the tests now you'll see that this is working. We're going to save our refactor step until after our next spec. Let's now add in a spec that checks for what happens when a guest user accesses the page:
# spec/features/static_spec.rb describe 'header' do it 'displays a welcome message to guest users' do expect(page).to have_content("Guest") end end
If you run the specs now you'll see that we didn't get a failure (mainly because I skipped a little ahead and put the current_user check in the view). So now let's refactor this code so that we don't have a nil check in the view. Let's start by updating our ApplicationController
code:
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. protect_from_forgery with: :exception def current_user super || OpenStruct.new(full_name: 'Guest') end end
I hope you paid attention in the Ruby programming course because this is going to leverage that knowledge in a few areas. Let's walk through what we're doing here:
We're overriding the Devise
current_user
method, since theApplicationController
is called after the Devise gem loads our new method will override the gem method.By calling
super
first, this means that we're telling Rails to simply use the Devise version ofcurrent_user
.If no user is logged in, in other words if
current_user
isnil
the pipes||
tell the method to instead return anOpenStruct
object that has one attribute:full_name
and it's set to"Guest"
.
This is much better behavior for the application to have, now we won't have to worry about our current_user
calls returning nil
and causing errors. Side note: this is a good pattern to follow for many other classes as well.
Now let's refactor our view code:
<!-- app/views/shared/_header.html.erb --> <strong>Welcome <%= current_user.full_name %></strong> <% if current_user.is_a?(OpenStruct) %> <%= link_to "Login", new_user_session_path %> or <%= link_to "Register", new_user_registration_path %> <% else %> <%= link_to "Logout", destroy_user_session_path, method: :delete %> <% end %>
So here we've moved our current_user.full_name
out of a conditional since we now know it won't be nil
. I'm keeping the login/register/logout
links in a conditional and we may come back and refactor this later on, but for right now I'm happy with how it's looking now. If you run rspec
now you'll see that all of the tests are passing and we're back to green. You can also test it out in the browser and see that this works when a user is logged in:
And when a guest user is on the site:
An added benefit to this implementation is that this doesn't only help out our header
partial, by setting an OpenStruct
our current_user
method now is guaranteed to work, even if another attribute is called. If you sign out and place the code <%= current_user.email %>
anywhere in your view and refresh, you'll see that the app doesn't break! Notice that we don't have an email
attribute in the OpenStruct
, however since the object exists the page will load normally.
Now that our view is reflecting the user login status, in the next lesson we're going to start implementing the permission structure, starting with installing the Pundit
gem.