Post

Manage your appointments with Rails, Part 2

Manage your appointments with Rails, Part 2

As I said last time (link to Part 1), in this blogpost we’ll continue with the same codebase and we’ll check how Devise manages user authentication. In our case, what we’re doing is a simple email/password login from an HTML form, but it might seem like black magic; the code that actually does the thing, is nowhere to be found…

You can see all the endpoints exposed by our application by running rails routes in the terminal. That’d actually be a lot of information, so lets see this instead:

All those endpoints, added by Devise, point to controllers that live inside the gem. If you visit one of those, e.g. http://localhost:3000/users/sign_in, you’ll see a login form. The views exist inside app/views/devise, but the controllers are inside of Devise. The rendered view is nothing but bare bones, pure HTML, with almost no styling. Let’s change that.

Get ClaCSSy

We’ll be using MaterializeCSS for styling the app. There’s a gem available (materialize-sass), so follow those directions to install and set the gem up. Don’t worry if you can’t find an app/assets/javascripts/application.js file in your project; Rails 6 doesn’t ship with an app/assets/javascripts folder, so you can just create it manually. Now, I’m going to update the styles for some of the views without giving much explanation (the only explanation is “I felt like this looks good”), but you can see all the changes here. You might find this syntax unknown:

1
<%= render 'shared/header' %>

This line tells Rails that we want to render a partial, a reusable partial view that does not belong to a controller. In the above case, a file named app/views/shared/_header.html.erb is rendered. Notice the underscore before the filename; that’s how Rails knows this is a partial view.

Now that Materialize is installed and we already have some styles in our app, we’ll start customising Devise’s views. You can find the Sign Up screen at app/views/devise/registrations/new.html.erb and the Log In screen at app/views/devise/sessions/new.html.erb, and my updated styles here. I also updated the password reset views for you.

Get into Devise

As you might’ve noticed by now, Devise handles everything that has to do with user sessions and registrations. It also manages password resets (in case any user forgets their password), email confirmation, social login (through a plugin), etc. So now is the time to create the first account by signing up. After signing up, open up a terminal window and run rails c (c stands for console). In the console, run:

1
User.first.username

Apparently, our username isn’t stored. And that’s because Devise has no idea what to do with this field. All it knows is that users register with an email and a password, nothing else. So, to permit this new parameter to be stored in the model, we need to let Devise know about it:

1
2
3
4
5
6
7
8
9
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end
end

Now, you can try again, and you’ll see that the username is stored in the database.

Now, we want users to be able to login using either their email, or their username. There are a couple of ways to do this, but we’ll use the official way.

You can check all my changed files here. There are a lot of changes in that commit, but all of them are coming from the docs and they give a good explanation on what everything does. In order to not confuse some emails with usernames (e.g., if some user sets their username to something that looks like email), I added some validations (and spec) to check if the username matches a certain RegExp. I’m not allowing special characters in the username, and they can’t start with a digit (for no real reason besides me not liking the usernames that start with digits).

Don’t forget about Rubocop

Rubocop is a dependency we installed on Part 1, and we didn’t really configure it. So if I run rubocop on terminal, I get 243 offenses. That’s not cool. That’s 243 times I’ve been added to Santa’s naughty list. So, let’s configure Rubocop and see how many “mistakes” I’ve really done.

Rubocop takes his orders from a file called .rubocop.yml. In this file, we’ll list all the rules we want to follow when writing code. Notice that this isn’t a standard way of writing Ruby. The good (and the bad) thing of most programming languages, Ruby included, is that you can write code in any way you want; the machine doesn’t care. But good programmers write code that other programmers understand. So, here’s my .rubocop.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
require: rubocop-rails
AllCops:
  TargetRubyVersion: 2.6.3
  Exclude:
    - 'db/**/*'
    - 'bin/**/*'
    - 'vendor/**/*'
    - 'config/**/*'
    - 'spec/spec_helper.rb'
    - 'spec/rails_helper.rb'
    - 'node_modules/**/*'
Rails:
  Enabled: true

# Commonly used screens these days easily fit more than 80 characters.
Metrics/LineLength:
  Max: 120

Metrics/AbcSize:
  Max: 20

# Too short methods lead to extraction of single-use methods, which can make
# the code easier to read (by naming things), but can also clutter the class
Metrics/MethodLength:
  Max: 20

# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
Metrics/ClassLength:
  Max: 1500

Metrics/ModuleLength:
  Max: 1500

# Messes with my RSpec cases
Metrics/BlockLength:
  Max: 500

Style/EmptyMethod:
  Enabled: false

# We do not need to support Ruby 1.9, so this is good to use.
Style/SymbolArray:
  Enabled: true

# Mixing the styles looks just silly.
Style/HashSyntax:
  EnforcedStyle: ruby19_no_mixed_keys

# has_key? and has_value? are far more readable than key? and value?
Style/PreferredHashMethods:
  Enabled: false

Style/CollectionMethods:
  Enabled: true
  PreferredMethods:
    # inject seems more common in the community.
    reduce: "inject"


# Either allow this style or don't. Marking it as safe with parenthesis
# is silly. Let's try to live without them for now.
Style/ParenthesesAroundCondition:
  AllowSafeAssignment: false
Lint/AssignmentInCondition:
  AllowSafeAssignment: false

# A specialized exception class will take one or more arguments and construct the message from it.
# So both variants make sense.
Style/RaiseArgs:
  Enabled: false

Style/FrozenStringLiteralComment:
  Enabled: false

# Indenting the chained dots beneath each other is not supported by this cop,
# see https://github.com/bbatsov/rubocop/issues/1633
Layout/MultilineOperationIndentation:
  Enabled: true

# Fail is an alias of raise. Avoid aliases, it's more cognitive load for no gain.
# The argument that fail should be used to abort the program is wrong too,
# there's Kernel#abort for that.
Style/SignalException:
  EnforcedStyle: only_raise

Style/RescueStandardError:
  EnforcedStyle: implicit

# Suppressing exceptions can be perfectly fine, and be it to avoid to
# explicitly type nil into the rescue since that's what you want to return,
# or suppressing LoadError for optional dependencies
Lint/HandleExceptions:
  Enabled: false

# do / end blocks should be used for side effects,
# methods that run a block for side effects and have
# a useful return value are rare, assign the return
# value to a local variable for those cases.
Style/MethodCalledOnDoEndBlock:
  Enabled: true

# Enforcing the names of variables? To single letter ones? Just no.
Style/SingleLineBlockParams:
  Enabled: false

# Shadowing outer local variables with block parameters is often useful
# to not reinvent a new name for the same thing, it highlights the relation
# between the outer variable and the parameter. The cases where it's actually
# confusing are rare, and usually bad for other reasons already, for example
# because the method is too long.
Lint/ShadowingOuterLocalVariable:
  Enabled: false

# Check with yard instead.
Style/Documentation:
  Enabled: false

# This is just silly. Calling the argument `other` in all cases makes no sense.
Naming/BinaryOperatorParameterName:
  Enabled: false

Now, running Rubocop will only show 6 offenses, which isn’t that bad. I can see 3 different issues that I have to fix:

  • Use %i[] for symbol arrays. Using [:username, :email] is the same as %i[username email]. Notice the 2nd form doesn’t have colons and comas.
  • Use find_by instead of where(…).first. Nothing wrong with where(…).first, just that where is used to return multiple entries, and find_by is used to return a single one.
  • Don’t use binding.pry… at least don’t push it on a production server.

You might want to run rubocop automatically whenever you’re ready to commit code in git. Since git is expandable with hooks, you can use a pre-commit hook that won’t commit your code if there are offenses. This goes beyond the Rails’ scope, hence beyond the blogposts scope, so consider it a homework.

Creating the User Dashboard

Now that the users authentication is ready, we can start thinking of actually building features for the app. I’ll start by creating a dashboard (a stubby one), where the users will get redirected after they log in, where they’ll see (and later, create and update) their available slots. Also, I don’t want the landing page to be visible for logged-in users; there’s no reason for them to see that page.

Firstly, I’ll create a DashboardController with an index action, and the view returned is the view where we’ll build the dashboard:

1
2
3
4
5
6
7
8
9
10
11
12
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # ...
  before_action :authenticate_user! # add this

  protected

  def after_sign_in_path_for(_) # add this
    dashboard_path
  end
  # ...
end
1
2
3
4
5
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  def index
  end
end
1
2
3
4
# app/views/dashboard/index.html.erb
<div class="valign-wrapper">
  <h2 class="center-align">Welcome <%= current_user.username %></h2>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# config/routes.rb
get 'dashboard', to: 'dashboard#index' # add this
# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  skip_before_action :authenticate_user!, only: :hello
  before_action :redirect_users, only: :hello

  def hello
  end

  private

  def redirect_users
    return unless user_signed_in?

    redirect_to dashboard_path
  end
end

We’re using before_action :authenticate_user! on ApplicationController to make sure all the routes require an authenticated user. And we skip it for the landing page, since we don’t need users to be authenticated to see the landing page. We’re also using a before_action to redirect already logged in users from the landing page to the dashboard.

Spec a bit

This is one of those cases when I’m too lazy to write specs before writing the code (no TDD involved here), but I’m also too lazy to manually test what I’ve implemented. So what we’re doing now is no more TDD, just writing automated tests.

I want to write some tests on how the system behaves when the login credentials or the sign up parameters are wrong, and what happens when a logged in user tries to access the landing page, or a non-logged in user tries to access the dashboard. Since I’ve already implemented these, I expect the specs to pass (given they’re written correctly).

Before actually writing the specs, I want to configure a couple of things:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Gemfile
group :development, :test do
  gem 'ffaker', '~> 2.12'  # add this
  # ...
end

group :test do
  gem 'factory_bot_rails', '~> 5.0', '>= 5.0.2' # add this
  # ...
end

# CREATE THE FOLLOWING FILES

# spec/support/factory_bot.rb
RSpec.configure do |config| 
  config.include FactoryBot::Syntax::Methods
end

# spec/support/headless_chrome.rb
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :selenium, using: :headless_chrome
  end
end

# spec/support/devise_helpers.rb
RSpec.configure do |config|
  config.include Devise::Test::IntegrationHelpers
end

# spec/factories/users.rb
FactoryBot.define do
  factory :user do
    username { FFaker::Lorem.words.join('') }
    email { FFaker::Internet.email }
    password { 'password' }
  end
end

What did we just do? We installed the FFaker gem to help us generate some fake test data, we installed and configured FactoryBot, we configured some Devise test helpers, and we configured a headless browser (chrome) to be used with the system specs. Now, for the real test, here’s how we test the login functionality (spec/system/login_spec.rb):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
require 'rails_helper'

RSpec.describe 'Login' do
  let(:user) { create :user }

  before do
    visit root_path
    expect(body).to have_link('Log In')
  end

  context 'with valid params' do
    let(:password) { 'password' }

    it 'logs in with username' do
      click_link 'Log In'

      within(:css, 'form') do
        fill_in 'Username or Email', with: user.username
        fill_in 'Password', with: password

        find('[type="submit"]').click
      end

      expect(page).to have_link('Log Out')
    end

    it 'logs in with email' do
      click_link 'Log In'

      within(:css, 'form') do
        fill_in 'Username or Email', with: user.email
        fill_in 'Password', with: password

        find('[type="submit"]').click
      end

      expect(page).to have_link('Log Out')
    end
  end

  context 'with invalid params' do
    let(:password) { '12345678' }

    it 'does not log in with username' do
      click_link 'Log In'

      within(:css, 'form') do
        fill_in 'Username or Email', with: user.username
        fill_in 'Password', with: password

        find('[type="submit"]').click
      end

      expect(page).to have_text('Invalid Login or password')
    end

    it 'does not log in with email' do
      click_link 'Log In'

      within(:css, 'form') do
        fill_in 'Username or Email', with: user.email
        fill_in 'Password', with: password

        find('[type="submit"]').click
      end

      expect(page).to have_text('Invalid Login or password')
    end
  end
end

This spec uses Capybara to emulate user behaviours. It’s clear that what we’re doing here, is describing what a typical user would do to login:

  • Visit the root path (/)
  • Click on the log in link in the navbar
  • Fill the form with their username/email and password
  • Click the Log in button

Finally, we expect to either see the logout button (if the login was done successfully), or an error message.

I added a couple of other spec files here, so check the diff.

Add a unique index for username

I just noticed that we don’t have an index for the username. This means that, when (if) our application gets thousands of users, searching through the database for a given username will take a while. Without indices, think of the database driver running a linear search for every record in the DB until it finds a matching username, or running out of entries. So, let’s do this:

1
$ rails g migration add_index_for_username
1
2
3
4
5
class AddIndexForUsername < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :username, unique: true
  end
end
1
$ rails db:migrate

To sumarize

In this blogpost you saw Rails partials in action, configuring Devise for your needs, configuring Rubocop, stopping users from accessing views you don’t want them to access, and writing system specs. If you get into an already existing project, Devise and Rubocop will probably be already configured. Writing (system) specs is something that you’ll do more often than you think, so better get used to the syntax and to the toolset as soon as possible. As for Devise and its plethora of configurable options, the wiki covers almost everything you need.

All the code for this project is in this github repo. Next time, we’ll jump into creating the Slot model. See you then.

Previously posted on my Medium blog

This post is licensed under CC BY 4.0 by the author.