Post

Manage your appointments with Rails, Part 1

Manage your appointments with Rails, Part 1

It’s trendy to assume Ruby is dead and the only thing keeping it alive is the Rails framework. It’s also trendy to start every Ruby-related blogpost with some “Ruby is dead” quotes when addressing to non-Ruby devs. Well, this isn’t that kind of blogpost. Instead, consider this a Ruby on Rails primer, a way to get into the Rails train (pun intended). I’m writing this series out of boredom, to give non-Ruby devs an entry point into web development with Rails. I’m not going over what Ruby and Rails are (I won’t even explain MVC to you), neither over how to install them. You can get enough information on that by Googling and if you are that lazy, check Ruby’s and Rails’ websites. If you need a reference, take a look at this. You can find the source code for this application here.

Without losing any time…

…let’s build this Rails application. Speaking of time, I want to build something similar to Calendly, but with a less beautiful UI (beauty is subjective) and with less features because we don’t have much time.

So, to create a new Rails app, run these in your terminal:

1
2
3
$ rails new skeduler
$ cd skeduler
$ rails server

These simple commands create a new Rails application called Skeduler (no relation to this other skeduler), then start the server so you can access your application. You can go to http://localhost:3000/ and see the greeting Rails page, which shows the running Ruby and Rails version. At this moment, the latest stable versions are respectively 2.6.3 and 6.0.0. I’d suggest you to always use the latest stable version when you start a new application.

A goal without a plan is just a wish

So let’s plan ahead what we will implement into Skeduler. As is the case with Calendly, we want our users (inside the system, let’s call them hosts) to create some slots: free hours on a day when other people (guests) can book a meeting with them. Without free slots, no booking can happen. After the slot is booked, no other guests can book the same slot (for obvious reasons).

We also want to remind the host and the guests before their scheduled meeting happens, possibly through an email or SMS at some time (e.g. 1 hour) before the meeting.

Install some gems

Ruby has been around for a while, and the good folks from the community have built more than a few libraries and helpful packages for almost everything you can think of. We call these gems, the files that helps us track these dependencies are Gemfile and Gemfile.lock. The first one lists all the dependencies we want in the app, and the other one is used internally from bundler. As you can see, Rails has already added some dependencies for us. Don’t mind them if you’re not already familiar with them. They all are necessary, in a way or another.

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
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.6.3'

gem 'bootsnap', '>= 1.4.2', require: false
gem 'devise', '~> 4.7' # add this
gem 'jbuilder', '~> 2.7'
gem 'puma', '~> 3.11'
gem 'rails', '~> 6.0.0'
gem 'sass-rails', '~> 5'
gem 'sqlite3', '~> 1.4'
gem 'turbolinks', '~> 5'
gem 'webpacker', '~> 4.0'

group :development, :test do
  gem 'pry-rails', '~> 0.3.9' # add this
  gem 'rspec-rails', '~> 3.8' # add this
end

group :development do
  gem 'web-console', '>= 3.3.0'
  gem 'listen', '>= 3.0.5', '< 3.2'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
  gem 'rubocop-rails', '~> 2.3', '>= 2.3.1' # add this
end

group :test do
  gem 'capybara', '>= 2.15'
  gem 'selenium-webdriver'
  gem 'webdrivers'
end

# add this
group :production do
  gem 'redis', '~> 4.0'
end

gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

I changed my Gemfile just a bit; I removed all the unnecessary comments and added some new dependencies. To get them, just run:

1
$ bundle install --without production

The gems I added are:

  • Devise — We’ll be using this for authenticating users
  • RSpec Rails — We’ll use this to write automated tests for the app
  • Redis (in production) — Needed to run ActionCable, Rails’ way of building WebSocket applications
  • Rubocop — Ruby linter to ensure good coding style
  • Pry — A shell that will become helpful while debugging.

The last step we need to take here is to run:

1
2
3
$ rm -rf test
$ rails generate rspec:install
$ rails generate devise:install

This will remove the default tests and will create a new directory (spec) where all the tests (or specs, if you will) will go. After that, it configures devise for our application. There are some steps listed by devise, which we have to manually follow right now. First, open config/environments/development.rb and when you see some lines starting with config.action_mailer, add this among those lines:

1
2
3
4
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
# add this
config.action_mailer.default_url_options = { host: localhost, port: 3000 }

Now, open app/views/layouts/application.html.erb and add these two lines just below the <body> tag:

1
2
3
4
5
<body>
  <p class="notice"><%= notice %></p>
  <p class="alert"><%= alert %></p>
  <!-- rest of body -->
</body>

Finally, run this to generate the devise views in app/views/devise:

1
$ rails g devise:views # g stands for generate

Your first endpoint

When first arriving at this application, we want to show users a landing page instead of that default Rails welcome page. Let’s start by creating a controller:

1
$ rails g controller pages hello --no-controller-specs --no-view-specs

This will create a PagesController with a hello method, and a view file with some text on it, and we’re skipping the creation of specs (we can do it by ourself). Now, open config/routes.rb and make it look like this:

1
2
3
Rails.application.routes.draw do
  root 'pages#hello'
end

We use root to make sure that whenever the app is opened, the newly-created view will get rendered. Now, start the server and you should see the new view.

The User model

I expect you to already know what a model is. We’ll let devise handle the creation of the user model, so run:

1
2
$ rails g devise User
$ rails db:migrate

This command will

  • Add migration file to create the users table with all the necessary data (the second command runs the migration)
  • Add user model file where device functionality is configured
  • Add the necessary routes for managing user session
  • Add user model spec file

And since we mentioned specs, let’s add some specs for the user. We already know that the user will have an email, a password with a minimum of 8 characters and a username that can’t be empty or duplicated. So let’s describe this in RSpec talk:

1
2
3
4
5
6
7
require 'rails_helper'

RSpec.describe User, type: :model do
  it 'has a unique email'
  it 'has a password of at least 8 characters'
  it 'has a unique username'
end

Now, if you run rspec on your terminal, you’ll see something like:

3 examples, 0 failures, 3 pending

Our tests aren’t really implemented (hence the pending status), so let’s implement them naively:

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
require 'rails_helper'

RSpec.describe User, type: :model do
  let(:email) { 'email@example.com' }
  let(:password) { 'password' }

  it 'has a unique email' do
    user1 = User.new(email: email, password: password)
    expect(user1).to be_valid
    user1.save

    user2 = User.new(email: email, password: password)
    expect(user2).not_to be_valid
  end

  it 'has a password of at least 8 characters' do
    user = User.new(email: email, password: 'a' * 7)
    expect(user).not_to be_valid

    user = User.new(email: email, password: 'a' * 8)
    expect(user).to be_valid
  end

  it 'has a unique username' do
    username = 'jamesbond'

    user1 = User.new(email: email, password: password, username: username)
    expect(user1).to be_valid
    user1.save

    user2 = User.new(email: email, password: password, username: username )
    expect(user2).not_to be_valid
  end
end

By running rspec you’ll see that some tests pass, and some of them fail for different reasons:

3 examples, 2 failures

The first spec passes, since Devise comes with some email validators out of the box. The second spec fails, because Devise validators require passwords to have at least 6 characters. The third spec fails because there’s no username field in the users table. We can fix the second spec by adding this to the user model:

1
2
3
4
5
6
7
8
9
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  # add this
  validates :password, length: { minimum: 8 }
end

This validator overrides the one bundled by Devise, so we now accept only passwords with 8 or more characters. As for the third spec, run this

1
$ rails g migration add_username_to_users username:string

This will create a new migration for us, but we need to change it a bit:

1
2
3
4
5
6
class AddUsernameToUsers < ActiveRecord::Migration[6.0]
  def change
    # add the unique: true flag
    add_column :users, :username, :string, unique: true
  end
end

Now, run the migration (rails db:migrate) and then run rspec. You’ll now see that all our specs are green.

Finally, let’s make our specs a bit shorter. In the Gemfile, on the test block, add this gem and run bundle install:

1
gem 'shoulda-matchers', '~> 4.1', '>= 4.1.2'

Next, create a file called specs/support/shoulda.rb and add this code inside (from the docs):

1
2
3
4
5
6
Shoulda::Matchers.configure do |config|
  config.integrate do |with|
    with.test_framework :rspec
    with.library :rails
  end
end

Now, open specs/rails_helper.rb and uncomment this line of code:

1
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

Finally, we can use Shoulda matchers to rewrite the same spec with less code:

1
2
3
4
5
6
7
require 'rails_helper'

RSpec.describe User, type: :model do
  it { should validate_uniqueness_of(:email).case_insensitive }
  it { should validate_length_of(:password).is_at_least(8) }
  it { should validate_uniqueness_of(:username).case_insensitive }
end

All those lines of code, condensed into just that. And now, just to verify that we didn’t break anything, run rspec again… and we broke something. The reason this happens is that we only have the uniqueness check for username in the database level, not in the model level. And Shoulda matchers work in the model level. So to fix the failure, add this in the User model:

1
2
3
validates :password, length: { minimum: 8 }
# add this
validates :username, uniqueness: { case_sensitive: false }

And now, once again, the specs are green.

To sumarize

In this blogpost you learned to Google what Ruby and Rails are, and you started creating a new Rails application. You installed a bunch of dependencies, some of which you got to use while testing. And you did some Test Driven Development (TDD), which is great. Even though most companies don’t actually do TDD (or BDD) all the time, they implement some sort of automated testing.

Next time we’ll continue with the same codebase and we’ll check how Devise manages user authentication. See you then.

Previously posted on my Medium blog

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