We are going to build a simple app to report and monitor incidents (an example of a similar app is here: http://inciweb.nwcg.gov/)

Create the app

rails new incident-reporter

Let's see if it works: rails s

Visit http://localhost:3000 and you should see "Yay! You're on Rails!"

Take a look at the generated directory structure.

Scaffold: Our first models

We want to create an Incident with a category, location and flags to say whether I am near or at the incident site. Something like this:

incident: {
"category": "string",
"latitude": "decimal",
"longitude": "decimal",
"at_location": "boolean",
"description": "text"
}

Rails offer a very quick way to start building a web app called scaffold. To create an Incident type the following in your shell

rails g scaffold incident latitude:decimal longitude:decimal at_location:boolean category:string description:text

This creates a model, a controller, a set of views and a migration. Take a look at the new files in app/controllers, app/models and app/views.

Before starting the server, you need to migrate the DB with the new table. Migrations are just Ruby classes

rails db:migrate # this is rake db:migrate in Rails < 5

Start the server and take a look around. Everything works like magic. You can CRUD incidents with 0 lines of code.

rails s

Take a look at:

  • Model: incident.rb
  • Controller: incidents_controller.rb
  • View: app/views/incidents/...
  • routes.rb how does it work? Take a look also at rake routes

Focus on the model: ActiveRecord

Active Record Basics

Active Record is the M (the model) in MVC. It is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database.

Object Relational Mapping

Using ORM (Object Relational Mapping), the properties and relationships of the objects in an application can be easily stored and retrieved from a database without writing SQL statements directly and with less overall database access code.

Active Record gives us several mechanisms, the most important being the ability to:

  • Represent models and their data.
  • Represent associations between these models.
  • Represent inheritance hierarchies through related models.
  • Validate models before they get persisted to the database.
  • Perform database operations in an object-oriented fashion.

Convention Over Configuration

With Convention Over Configuration, you must follow some conventions. So, for a model called Book, you should have a database table called books. In fact:

  • Database Table - Plural with underscores separating words (e.g., book_clubs).
  • Model Class - Singular with the first letter of each word capitalized (e.g., BookClub). Models are Ruby classes!

There are conventions also for some fields in database tables:

  • Primary keys: by default, Active Record will use an integer column named id as the table's primary key. When using Active Record Migrations to create your tables, this column will be automatically created.
  • Foreign keys: These fields should be named following the pattern singularized_table_name_id (e.g., item_id, order_id). These are the fields that Active Record will look for when you create associations between your models.

There are also some optional column names that will add additional features to Active Record instances:

  • created_at: Automatically gets set to the current date and time when the record is first created.
  • updated_at: Automatically gets set to the current date and time whenever the record is updated.

CRUD: Create, Read, Update, Delete with ActiveRecord

At this point, we have a new Incident resource. We also have routes to manage it. Let's us take a look at the routes (It may take some seconds...) by visiting localhost:3000/rails/info/routes. You can also view them in your shell by typing:

$ rails routes

A list of new rules should appear. All these routes in fact are resulted from a single line written in the routes.rb file.

config/routes.rb
resources :incidents

Now, let's us play a bit with ActiveRecord! Open a Rails console.

$ rails console

Create

Let's create a new incident. The method .new as you might expect creates a new Article with nil values for all its attributes (id: nil, title: nil, body: nil, ...). The .save method allow us to store the created incident in the database. Note that when you save an incident in the database the id attribute is automatically incremented.

i = Incident.new
puts i
i.save

Let's add another one:

j = Incident.new(latitude: -33.97, longitude: 24.63, at_location: true, category: 'Fire', description: 'Propella is on fire!')
puts j
j.save

Instead of using .new and then .save you can just use .create to obtain the same result.

Read

There are a lot of default methods that allow you to read data from the database.

incidents = Incident.all # Returns a list of all the incidents stored in the database.
one = Incident.find(1) # Returns the incident for which its corresponding primary key (id) has value 1.
first = Incident.first # Returns the first incident stored.
last = Incident.last # Returns the last incident stored.
fire = Incident.find_by(category: 'Fire') # Returns the first incident with category 'Fire'
fires = Incident.where(category: 'Fire') # Returns all the incidents with category 'Fire'

Update

You can edit or update an existing article as follows:

first = Incident.first # Gets the first incident
first.category = "Water" # Changes its title
first.save # Stores it

However, you can also write it down in the following way:

first = Incident.first
first.update(at_location: false, latitude: 11, longitude: 46, description: 'Whatever')

Delete

In order to delete or remove an article form the database, let's use the .destroy method.

first = Incident.first
first.destroy # Or first.delete

You can also delete all the articles as follows

Incident.destroy_all # Or Incident.delete_all

Note that the methods destroy and delete, destroy_all and delete_all are not the same.

If you want to read more, you can find useful information here.

Focus on the views

The layout

The default layout for the views of our application is application.html.erb. It acts like a container for all the HTML that will be rendered by the controllers. When a controller renders a view in response to a method call, it looks for the layout and replaces yield with the compiled view for the invoked method.

IncidentsController#show

Look at app/views/incidents/show.html.erb and change it a bit to learn ERB.

Where does @incident come from? Look at the controller's before_action :set_incident

app/controllers/incidents_controller.rb
before_action :set_incident, only: [:show, :edit, :update, :destroy]

What happens if I call @pippo in my view? This will be an instant variable with value nil. If I try to call pippo, it will look for a local variable or method called pippo (see the error message).

We can change a few things just as an example

app/views/incidents/show.html.erb
<h1>
Incident #<%= @incident.id %> (<%= @incident.category %>)
</h1>
<p>
<%= @incident.description %>
</p>
<hr>
<p>
Occurred <%= @incident.at_location? ? 'at' : 'near' %>
(<%= @incident.latitude %>, <%= @incident.longitude %>)
</p>
<p>
The reporter is <%= !@incident.at_location? ? 'not' : '' %> on location
</p>

Exercise! Try doing something interesting with index.html.erb. Notice the use of @incidents.each. For example:

app/views/incidents/index.html.erb
<%= link_to 'New Incident', new_incident_path %>
<% @incidents.each do |incident| %>
<div>
<h2>
<%= incident.category %>
<%= incident.at_location? ? 'at' : 'near' %>
(<%= incident.latitude %>, <%= incident.longitude %>)
</h2>
<p>
<%= incident.description %>
</p>
<p>
The reporter is <%= !incident.at_location? ? 'not' : '' %> on location
</p>
<p>[
<%= link_to 'Show', incident %> |
<%= link_to 'Edit', edit_incident_path(incident) %> |
<%= link_to 'Destroy', incident, method: :delete, data: { confirm: 'Are you sure?' } %>
]
</p>
<hr>
</div>
<% end %>

The form (incidents/_form.html.erb)

This is a partial (more on these later) that uses several helpers to simplify the creation of forms (if you use the simple_form gem you get even simpler helpers... but more headaches). Take a look and play around a bit.

Validations

Rails models can declare validations that are automatically run every time we try to write to the db. For example, we want every incident to have coordinates.

app/models/incident.rb
validates_presence_of :latitude, :longitude
# which is the same as (more readable and preferred)
validates :latitude, presence: true
validates :longitude, presence: true

As you know, latitude and longitude have to be within specific ranges. So we can add the following validations to our Incident model.

app/models/incident.rb
validates :latitude , numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }
validates :longitude, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }

Now try entering invalid data in the form and see what happens.