Rails Appointments Project

Dakota Lee Martinez
Jun 1, 2016 · 14 min read

The Flatiron School’s Learn Verified program has milestone assessments as part of the curriculum. The goal of these projects is to allow students to demonstrate their understanding by applying their knowledge of multiple concepts introduced throughout a section of the course. In this case, the Rails Assessment challenges students to build a content management system using Ruby on Rails that manages related data using complex forms and nested RESTful routes. See the repository on github for a full explanation of the requirements and the walkthrough to check out my app.

Rails Appointments Manager Feature Walkthrough

To fulfill the requirements for the project, I prepared an app designed to keep track of appointments with multiple clients in multiple locations. In this post, I’ll be walking you through how I overcame some of the challenges that came up in building out the application and implementing one of the main features — complex forms that write to associated resources and display differently on nested RESTful routes. I’ll be focusing on the appointment form and how I made ample use of helper methods and partials to put together a form that would display differently on three different possible routes: /appointments/new, /clients/:id/appointments/new & /locations/:id/appointments/new.

In this post, I’ll give you all an overview of the following tasks that are required to build this application:

  1. Models and Associations
  2. Validating Appointment Times
  3. Building a Complex Form that writes to Associated Models and responds to Nested Routes
  4. Constructing multiple partials called by helper methods within the form partial to display the same form differently on different routes.
  5. Configuring the Model, View, and controller to accept a datetime input from the Appointment form.
  6. Using the simple_calendar gem to display the appointments on the calendar

Models and Associations

Before we get started, I want to introduce you to the project. If you’d like to take a look, I’ve included links to a working demo and the github repository. Starting with the model layer and the associations between my objects, I had four models: User, Location, Client and Appointment. Locations, clients and appointments all belong to a user, so each user can interact with their own information privately. The Appointment model is where most of the heavy lifting is done, keeping track of associations between a user’s clients and locations and the income that they earned from each.

Models

class User < ActiveRecord::Base 
has_many :clients
has_many :appointments
has_many :locations
...endclass Location < ActiveRecord::Base
belongs_to :user
has_many :appointments, dependent: :destroy
has_many :clients, through: :appointments
...endclass Client < ActiveRecord::Base
belongs_to :user
has_many :appointments, dependent: :destroy
has_many :locations, through: :appointments
...endclass Appointment < ActiveRecord::Base
belongs_to :user
belongs_to :location
belongs_to :client
...end

And here is a look at the schema for your reference:

ActiveRecord::Schema.define(version: 20160420012901) docreate_table "appointments", force: :cascade do |t|
t.datetime "appointment_time"
t.integer "duration", limit: 4
t.float "price", limit: 24
t.integer "location_id", limit: 4
t.integer "user_id", limit: 4
t.integer "client_id", limit: 4
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_inex "appointments", ["client_id"], name: "index_appointments_on_client_id", using: :btree
add_index "appointments", ["location_id"], name: "index_appointments_on_location_id", using: :btree
add_index "appointments", ["user_id"], name: "index_appointments_on_user_id", using: :btree
create_table "clients", force: :cascade do |t|
t.string "name", limit: 255
t.string "phone_number", limit: 255
t.string "email", limi: 4
t.integer "user_id", limit: 4
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "clients", ["user_id"], name: "index_clients_on_user_id", using: :btreecreate_table "locations", force: :cascade do |t|
t.string "nickname", limit: 255
t.string "city", limit: 255
t.string "street_address", limit: 255
t.string "state", limit: 255
t.string "zipcode", limit: 255
t.string "business_name", limit: 255
t.integer "user_id", limit: 4
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
t.string "email", limit: 255, default: "", null: false
t.string "encrypted_password", limit: 255, default: "", null: false
t.string "reset_password_token", limit: 255, default: "", null: false
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", limit: 4, default: 0, null: false
t.datetime: "current_sign_in_at"
t.datetime: "last_sign_in_at"
t.string "current_sign_in_ip", limit: 255
t.string "last_sign_in_ip", limit: 255
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "provider", limit: 255
t.string "uid", limit: 255
end
add_index "users", ["email"], name: "index_users_on_email", unique: ture, using: :btree
add_index "users", ["provider"], name: "index_users_on_provider", using: :btree
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["uid"], name: "index_users_on_uid", using: :btree
add_foreign_key "appointments", "clients"
add_foreign_key "appointments", "locations"
add_foreign_key "appointments", "users"
end

Important Applications

This app could be used by a real estate agent to keep track of which clients have been shown a particular property. Due to a recent change in real estate law, if a seller decides to list their property with another agent, the listing agent whose listing is about to expire must produce a list of the clients that have seen the property by the end of the day, or they are not entitled to a commission — even if the property eventually sells to one of those clients. Using Rails Appointments to keep track of property showings allows the agent to easily produce such a list.

This app would also be ideal for use by a freelancing music teacher who also teaches at multiple music schools, a physical trainer who works at multiple gyms, a yoga instructor who teaches at multiple studios and also privately. Rails appointments can keep track of the income produced by multiple clients and locations, allowing for an easy overview of monthly income.

Validating Appointments

The most challenging aspect of putting together this application for me was dealing with Ruby datetime objects. First, there was the matter of making sure that users would not be able to book conflicting appointments. This involves a validation which compares a new appointment’s start and end times with all of the current user’s nearby appointments, making sure that neither the starting nor the ending times overlap with any other appointments:

include ActiveModel::Validationsclass AppointmentTimeValidatordef initialize(appointment)
@appointment = appointment
@user = appointment.user
end
def validate# selects the user’s appointments from yesterday,
# today and tomorrow
appointments = @user.appointments.select { |a| a.appointment_time.midnight == @appointment.appointment_time.midnight || a.appointment_time.midnight == @appointment.appointment_time — 1.day || a.appointment_time.midnight == @appointment.appointment_time + 1.day }
# makes sure that current appointments don’t overlap
# first checks if an existing appointment is still
# in progress when the new appointment is set to start
# next checks if the new appointment would still be in
# progress when an existing appointment is set to start
appointments.each do |appointment|if @appointment != appointment
if appointment.appointment_time <= @appointment.appointment_time && @appointment.appointment_time <= appointment.end_time || @appointment.appointment_time <= appointment.appointment_time && appointment.appointment_time <= @appointment.end_time
@appointment.errors.add(:appointment_time, “is not available.”)
end
end
end
end
end # End AppointmentTimeValidator
validate do |appointment|
AppointmentTimeValidator.new(appointment).validate
end

The last example in this section of the Rails Guide on ActiveRecord Validations was helpful in learning how to apply this type of validation.

Next there was the matter of dealing with Time zones. I had an upcoming_appointments instance method for the User class, allowing me to display upcoming appointments for the current user. But, when I created an appointment that was only an hour in the future, it was not appearing in the list of upcoming lessons because I had not taken the time zone into account. The UTC offset in my location on the west coast of the United States is -7 hours, so any appointment scheduled for less than 7 hours in the future was not appearing in the upcoming lessons list — not ideal.

I was able to solve the problem by adjusting a method in my Appointment model that was used to parse the appointment time data submitted to the appointment form. Originally, I was creating a new datetime by parsing the input from the form in UTC, so the stored times did not match up with the times that needed to be displayed in the front end. Here’s what I had:

def appointment_time=(time)
if time.is_a?(Hash)
self[:appointment_time] = parse_datetime(time)
else
self[:appointment_time] = time
end
end
def parse_date(string)
array = string.split(“/”)
first_item = array.pop
array.unshift(first_item).join(“-”)
end
def parse_datetime(hash)
DateTime.parse(“#{parse_date(hash[“date”])} #{hash[“hour”]}:#{hash[“min”]}”)
end

The appointment_time= method is a custom setter that’s called upon receiving a hash (through mass assignment) from the appointments#create action. (The setter is also written to accept a time object directly for testing purposes). When the appointment_time= method is called on a hash, the parse_datetime method is called on the hash to produce a datetime object that can be saved into the database. The problem here is that I was parsing that object without regard to the time zone, so I was getting problems with the UTC offset.

Fortunately, it wasn’t too complicated to fix. All I had to do to resolve this was to make a small adjustment to my parse_datetime method:

def parse_datetime(hash)
Time.now.parse(“#{parse_date(hash[“date”])} #{hash[“hour”]}:#{hash[“min”]}”)
end

After making this change, the hours were going in properly.

Building A Complex Form with Nested Resources

The most important page in this application is the new/edit appointment Page. I wanted to be sure that users would be able to look at their calendar while booking a new appointment, as that would make the app much more convenient and useful. This is what I wanted the user experience to look/feel like:

New Appointment Form

This page allows users to do a lot of things:

  • Select from a list of Clients already in the system, or create a new client
  • Choose a date and time for the new appointment
  • Choose a duration for the appointment
  • Choose a price for the appointment (optional)
  • Select from a list of Locations already in the system, or create a new location (both optional)

One of the requirements for this assessment is that we implement nested routes. In this case, if we were to create a new client named “Dakota,” we could visit the new appointment page through a nested route to make a new appointment with Dakota:

Link from Clients Page to Nested Route for Appointments
Nested Route for New Appointments (notice this appointment is with dakota)

I also implemented similar functionality with a nested route for locations, allowing the creation of a new appointment at a given location by clicking a link from the Locations index page. This is quite convenient for the Real Estate usage of this application, allowing easy creation of a new showing at one of the listed properties. Here is an excerpt from config/routes.rb for your reference:

Rails.application.routes.draw do
resources :appointments
resources :locations do
resources :appointments, only: [:index, :show, :new]
end
resources :clients do
resources :appointments, only: [:index, :show, :new]
end

Constructing the Appointments Form Partial

Because the appointments form is a convergence point of all of the other components of the application, keeping it simple and clean in the view layer was super important. I’ve made heavy use of helpers and partials to do just that. Here’s a look at the appointments form partial: (app/views/appointments/_form.html.erb)

<%= render partial: “layouts/errors”, locals: { object: appointment } %>
<%= form_for appointment do |f| %>
<%= client_fields(f, appointment, client) %>
<div class=”field”>
<p>
<%= label_tag “appointment[appointment_time][date]”, “Date” %>
<%= text_field_tag “appointment[appointment_time][date]”, appointment_date(appointment), class: “datepicker” %>
</p>
</div>
<div class=”field”>
<p id=”time”>
<%= label_tag “appointment[appointment_time][hour]”, “Time” %>
<%= hour_selector(“appointment[appointment_time][hour]”, appointment) %> :
<%= min_selector(“appointment[appointment_time][min]”, appointment) %>
</p>
</div>
<div class=”field”>
<p>
<%= f.label :duration %>
<%= duration_hour_field(“appointment[duration][hour]”, appointment) %>
<%= duration_minute_field(“appointment[duration][min]”, appointment) %>
</p>
</div>
<div class=”field”>
<p>
<%= f.label :price %>
<%= f.number_field :price, step: 0.25, value: float_two_decimals(appointment.price) %>
</p>
</div>
<%= location_fields(f, appointment, location) %>
<p><%= f.submit %></p>
<% end %>

Taking it from the top, I’ve implemented a partial to handle displaying the errors that can handle displaying errors for clients, locations and appointments (app/views/layouts/_errors.html.erb):

<% if object && object.errors.any? %>
<h2><%= “#{object.errors.full_messages.uniq.count} error(s):” %></h2>
<ul>
<% object.errors.full_messages.uniq.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>

Next I’ve created the form by opening using the form_for helper and creating the form builder object ( |f| ). The first thing I do is invoke a custom helper method named client_fields to which I’m passing three arguments: the form builder object, the appointment, and the client.

Helpers & Partials for Appointments form on Nested Routes

This is where it gets a little tricky. There are quite a few files that are interacting with one another here to make everything come together. To make this a little clearer, let’s walk through what happens when a user visits the following url: /clients/1/appointments/new. This is an excerpt from the appointments controller (app/controllers/appointments_controller.rb):

class AppointmentsController < ApplicationController
before_action :authenticate_user!
before_action :set_client, only: [:index, :new, :edit]
before_action :set_location, only: [:index, :new, :edit]

...
def new
@appointments = current_user.appointments.select { |a| a.persisted? }
@appointment = current_user.appointments.build
end

private

def set_client
@client = current_user.clients.find_by(id: params[:client_id])
end

def set_location
@location = current_user.locations.find_by(id: params[:location_id])
end

end

You can see here that when a user visits /appointments/new, the Appointments#new action is fired. Before the action is fired, the set_client and set_location methods are fired. These methods check to see if the current user has a client or location matching the parameter passed by the nested route in the url. In this case, the @client instance variable is assigned to the current user’s client whose id is 1. Since params[:location_id] is nil, the @location instance variable is set to nil.

So the Appointments#new action passes @appointment (a new appointment that belongs to the current user) and @client to the view. Now let’s take a look at how the form partial is being called in the view (app/views/appointments/new.html.erb):

<h1>New Appointment</h1>
<%= render partial: ‘form’, locals: { appointment: @appointment, client: @client, location: @location } %>

Okay, so we’re passing @appointment and @client to the partial and they both have values. Let’s take a look at the relevant code from the form partial again (app/views/appointments/_form.html.erb):

# app/views/appointments/_form.html.erb
<%= form_for appointment do |f| %>
<%= client_fields(f, appointment, client) %>
# app/helpers/appointments_form_helper.rb
def client_fields(form, appointment, client)
if client
output = [content_tag(:h3, "with #{client.name}"), hidden_field_tag("appointment[client_id]", client.id)]
safe_join(output)
else
render partial: "client_fields", locals: { f: form, appointment: appointment }
end
end

All right, now we’re getting somewhere. So we’re passing three arguments to the client_fields helper method: the form builder object, the appointment object, and the client object. In this case, because we are on a nested client route (/clients/1/appointments/new), the client variable has a value (not nil) and the helper will output an h3 tag saying “with dakota” and a hidden field tag to submit dakota’s client id.

The reason we’re passing the form builder object to this helper is due to the other case (where we’re not on a nested route). In that case we’ll need to render the client_fields partial which makes use of the form builder object (app/views/appointments/_client_fields.html.erb):

<% if current_user.clients.any? %>
<div class=”field”>
<p>
<%= label_tag :client %>
<%= collection_select :appointment, :client_id, current_user.clients, :id, :name, { prompt: true } %>
</p>
</div>
<% end %>
<p><% if current_user.clients.any? %>Or <% end %>
Create a New Client</p>
<div class=”field”>
<%= f.fields_for :client, Client.new do |ff| %>
<%= ff.label :name %>
<%= ff.text_field :name %>
<% end %>
</div>

If the user has already created at least one client, they will see a select field with a list of their current clients as options followed by a text field to create a new client. If the user hasn’t created any clients yet, they will only see the text field to create a new client. If the user visits the new appointment page from a nested client route (/clients/1/appointments/new), they won’t see either of these fields because this partial will not be rendered by the client_fields helper method.

The methodology for implementing this functionality on the nested route for locations (/locations/1/appointments/new) is very similar. Once this functionality is built, there is one major hurdle left to cross before the application is fully functional.

Handling Date Input

Going from the form on the front end to a proper datetime object on the back end was another big challenge in building this application. Getting this working required the help of the Model, View and Controller. We’ve already discussed how the model handles parsing the information it receives from the controller into a DateTime object that is stored as the value of an appointment’s appointment_time property, but let’s review. Here is the model code that handles setting the appointment time:

def appointment_time=(time)
if time.is_a?(Hash)
self[:appointment_time] = parse_datetime(time)
else
self[:appointment_time] = time
end
end
def parse_date(string)
array = string.split(“/”)
first_item = array.pop
array.unshift(first_item).join(“-”)
end
def parse_datetime(hash)
Time.now.parse(“#{parse_date(hash[“date”])} #{hash[“hour”]}:#{hash[“min”]}”)
end

Now let’s look at the part of the form that displays the appointment time fields:

<div class=”field”>
<p>
<%= label_tag “appointment[appointment_time][date]”, “Date” %>
<%= text_field_tag “appointment[appointment_time][date]”, appointment_date(appointment), class: “datepicker” %>
</p>
</div>
<div class=”field”>
<p id=”time”>
<%= label_tag “appointment[appointment_time][hour]”, “Time” %>
<%= hour_selector(“appointment[appointment_time][hour]”, appointment) %> :
<%= min_selector(“appointment[appointment_time][min]”, appointment) %>
</p>
</div>
<div class=”field”>
<p>
<%= f.label :duration %>
<%= duration_hour_field(“appointment[duration][hour]”, appointment) %>
<%= duration_minute_field(“appointment[duration][min]”, appointment) %>
</p>
</div>

And finally let’s take a look at how the controller is expecting to receive this information through strong parameters (app/controllers/appointments_controller.rb):

def appointment_params
params.require(:appointment).permit(:client_id, :price, :location_id, location_attributes: [:nickname], client_attributes: [:name], appointment_time: [:date, :hour, :min], duration: [:hour, :min])
end

After we’ve successfully configured the form to handle date input and set the appointment time in the proper time zone, all we have to do is display the appointments on the calendar.

Displaying the Calendar

Another challenge I faced when building this project was managing potentially complicated views. In putting together the interface, I thought that it was very important to be able to look at the calendar while editing or making a new appointment. I had previously built my own calendar for this project when I was building it in Sinatra, but was a little wary of taking that on in the face of also wanting to implement easy navigation between different weeks/months.

I ended up using the simple_calendar gem for the skeleton of my calendar and combining it with the inner workings of the code from my hand-coded calendar, making ample use of helpers and partials to reduce the clutter in the view layer. This allows me to give a visual representation of how long an appointment is on the weekly calendar (in the form of a shaded box) that links to the edit form, making it super simple to reschedule appointments.

Simple Calendar allows you to easily customize their views by running a generator and then editing to your heart’s content:

rails g simple_calendar:views

I made a couple of modifications to the app/views/simple_calendar/_week_calendar.html.erb partial and then created my own partial to handle displaying events with the shaded background.

Here is the source code from the gem:

<div class="simple-calendar">
<div class="calendar-heading">
<%= link_to I18n.t('simple_calendar.previous', :default => "Previous"), calendar.url_for_previous_view %>
<span class="calendar-title">Week <%= start_date.strftime("%U").to_i %></span>
<%= link_to I18n.t('simple_calendar.next', :default => "Next"), calendar.url_for_next_view %>
</div>

<table class="table table-striped">
<thead>
<tr>
<% date_range.slice(0, 7).each do |day| %>
<th><%= I18n.t("date.abbr_day_names")[day.wday] %></th>
<% end %>
</tr>
</thead>

<tbody>
<% date_range.each_slice(7) do |week| %>
<tr>
<% week.each do |day| %>
<%= content_tag :td, class: calendar.td_classes_for(day) do %>
<% if defined?(Haml) && respond_to?(:block_is_haml?) && block_is_haml?(block) %>
<% capture_haml(day, sorted_events.fetch(day, []), &block) %>
<% else %>
<% block.call day, sorted_events.fetch(day, []) %>
<% end %>
<% end %>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>

And here is the same file after my modifications to display the dates of the current week at the top of the calendar and the dotted lines indicating quarters of an hour within each day:

<div class=”simple-calendar weekly”>
<div class=”header”>
<h2><%= start_date.strftime(“%b %Y”) %></h2>
<h3>
<%= link_to “Previous”, calendar.url_for_previous_view, class: “btn” %>
Week of <%= start_date.strftime(“%b %d”) %> to <%= (start_date + 6.days).strftime(“%b %d”) %>
<%= link_to “Next”, calendar.url_for_next_view, class: “btn” %>
</h3>
</div>
<table class=”table table-striped”>
<thead>
<tr>
<% date_range.slice(0, 7).each do |day| %>
<th><%= I18n.t(“date.abbr_day_names”)[day.wday] %></th>
<% end %>
</tr>
</thead>
<tbody>
<% date_range.each_slice(7) do |week| %>
<tr>
<% week.each do |day| %>
<%= content_tag :td, class: calendar.td_classes_for(day) do %>
<%= day.strftime(“%e”) %>
<% 52.times do %>
<div class=”quarter”></div>
<% end %>
<% if defined?(Haml) && respond_to?(:block_is_haml?) && block_is_haml?(block) %>
<% capture_haml do %>
<% block.call day, sorted_events.fetch(day, []) %>
<% end %>
<% else %>
<% block.call day, sorted_events.fetch(day, []) %>
<% end %>
<% end %>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>

And here’s the partial that I added for reference within the index view (app/views/simple_calendar/_weekly_calendar.html.erb):

<%= week_calendar events: appointments do |date, appointments| %>

<% appointments.each do |appointment| %>
<div class=”appointment<%= highlight_appointment(appointment) %>” style=”<%= appointment_position(appointment) %> <%= appointment_height(appointment) %>”>
<%= link_to(appointment_text(appointment), edit_appointment_path(appointment) ) %>
</div>
<% end %>
<% end %>

And here are the appointments_calendar helper methods that are used both to call this partial and add the classes, styles and links:

module AppointmentsCalendarHelper
def add_weekly_calendar(appointments)
render partial: “simple_calendar/weekly_calendar_appointments”, locals: { appointments: appointments }
end

def appointment_position(appointment)
“top: #{ ( ( (appointment.appointment_time — appointment.appointment_time.midnight)/3600–8 ) * 40 ) + 28}px;”
end

def appointment_height(appointment)
“height: #{appointment.duration * 40/3600}px;”
end

def at_location(appointment)
“ at #{appointment.location_name}” if appointment.location
end

def highlight_appointment(appointment)
if current_page?( appointment_path(appointment)) || current_page?( edit_appointment_path(appointment) )
“ highlight”
end
end

def appointment_text(appointment)
“<span class=’name’>#{appointment.client_name}</span>#{at_location(appointment)}”.html_safe
end

def from_to(appointment)
“#{appointment.start_time.strftime(“%l:%M %p”)} — #{appointment.end_time.strftime(“%l:%M %p”)}”
end

def date_of_next(day, start_date)
date = Date.parse(day)
delta = date >= start_date ? 0 : 7
date + delta
end

def date_of_last(day)
date_of_next(day) — 7.days
end
end

Thank you for Reading

I hope you have enjoyed this walkthrough of how I built an appointments manager that takes advantage of nested routes and complex forms with nested resources. If there’s any part of the process that you’d like more detail or clarification about, or if you have an idea about how to extend or improve the application, please do let me know in the comments section.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade