2503ICT: Rails 2 - Basic Patterns


Under construction!

List-detail pattern

Action index displays a list of item summaries. Each item summary is (or has) a link to action show which displays the details of that item.

Action index:

def index
  @items = Item.all
end

View index.html.erb:

<ul>
<% @items.each do |item| %>
  <li><%= link_to item.name, item %>
<% end %>
</ul>

Note that scaffolds do not implement this pattern as they display a list of item details.

It may be possible to select only the summary fields in action index as an optimisation but I'm not sure if this works or not:

def index
  @items = Item.select(:id, :name).all
end

(Information obtained here.)

Form validation errors

Validation errors in form entry should be displayed with the re-rendered form. So, in views new.html.erb and edit.html.erb, forms should start like this:

<%= form_for @item do |f| %>
  <%= render 'shared/error_messages', form: f.object %>
  ...

Directory views/shared/ should contain the partial _error_messages.html.erb:

<% if form.errors.any? %>
  <div class="alert">
    The form contains <%= pluralize(form.errors.count, "error") %>.
    <ul>
    <% form.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>

Note that when the form is re-rendered the data entered is re-displayed, so the user does not have to re-enter it. This is good practice.

Using selections in forms

When a field value in a form (e.g., category_id) is to be chosen from a finite number of values stored in a database, the form should use a select element with options normally chosen from the database.

For example, action new in items_controller.rb:

class ItemsController < ApplicationController
  def new
    @item = Item.new
    @categories = category_options
  end

  ...

  private

    def category_options
      Category.all.collect { |c| [c.name, c.id] }
    end
end

The private method category_options returns a value of the form

[["Books", 1], ["Movies", 2]]

This is another way of selecting only some fields from each object in a collection.

View new.html.erb:

  <%= f.label :category_id %>
  <%= f.select(:category_id, options_for_select(@categories)) %>

The generated form has the corresponding element:

  <label for="item_category_id">Category</label>
  <select id="item_category_id" name="item[category_id]">
    <option value="1">Books</option>
    <option value="2">Movies</option>
  </select>

Other variants of this approach are also possible.

To select the currect option in the selection, write something like the following.

<%= f.select(:category_id, 
             options_for_select(@categories, selected: @item.category_id)) %>

As an aside, I think forms look best as tables.

Render vs redirect

After an unsuccessful attempt to update the database by creating a new object or updating an existing object, you must re-render the form (with error messages, and user text).

After a successful attempt to update the database, you must redirect to a page where the user can see the results of that update. Typically, after creating and updating an object, you should show that object; after deleting an object, you should show the list of objects (which no longer contains the deleted object).

As a technical point, render can refer to a relative path (e.g., /objects/m) but redirect must refer to a complete URL (e.g., http:/example.com/objects/m). FOrtunately, statements that define paths in config/routes.rb define both paths (e.g., root_path) and URLs (e.g., root_url).

E.g., in actions create and update.

User-defined searches

No doubt there are many ways to do this. Here is one basic approach.

We use the same action items#index to display the list of items and also the list of all items that are the result of a search. After all, these are both just lists of items. It follows that we use the same page, items/index.html.erb, for all lists of items.

We now have to decide where to put the search form. One reasonable possibility is to put it at the top of the page items/index.html.erb. That way, we can see the search query with the results. Another possibility is to put the form in the application header, so it is always available. Let's assume the first choice. The form has the form (sic):

<h1>Search for items</h1>

<%= form_tag items_path, method: 'get' do %>
  <p>
    <%= label_tag :query, "Search for:" %>
    <%= text_field_tag :query, params[:query] %>
    <%= submit_tag "Search", name: nil %>
  </p>
<% end %>

This action of the form is items_path, which is routed to the action items#index. We could write the search code in action index, but it is preferable (so the Railscast says) to do the search in the items model. So, action index should be defined as follows:

  def index
    @items = Item.search(params[:query])
  end

And the search should be defined in the items model, models/item.rb as follows:

class Item < ActiveRecord::Base
  ...
  
  def Item.search(query) # a class method
    if !query.blank?
      Item.where('name like ?', "%#{query}%")
    else
      Item.all
    end
  end
end

You can see that this searches for all items whose name contains the query string. Generalisations are now straightforward.

References

Migrations

Migrations are used to change the underlying relational database structure buased on changes to the Rails models definitions. Migrations are Ruby classes stored in the folder db/migrate. The name of every migration contains the timestamp when it was defined.

It is possible to manually edit these migration files as required.

Migrations are executed in increasing order of their timestamps by the following command:

rake db:migrate

Single migrations can be undone with the following command:

rake db:rollback

Creating tables

This may be the most common migration. After generating a model, e.g., Item, in Rails, a migration with the name timestamp_create_items.rb is automatically created. This file shows the names and types of the fields in the model, and hence of the columns in the corresponding table.

Adding columns

To add a field to a model, and hence a column to the corresponding table, use a command like the following.

rails generate migration add_location_to_items location:string

This command adds a column location of type string to the table items.

Rails is able to recognise that the argument add_location_to_items has the form add_column_to_table.

Removing columns

Similarly, it is possible to remove columns from a table with commands like the following.

rails generate migration remove_location_from_items location:string

Adding indexes

It is possible to add an index to the column name of the table items as follows.

rails generate migration add_index_to_items_name

(It is possible that in each of these migrations, the argument needs to be written in CamelCase instead of snake_case.)

In all cases, it is necessary to take care with respect to the definition and execution of migrations. You can always check the resulting database structure to see that the migrations have been performed correctly.

Reference

Creating sample data

It's desirable to define sample data in a file so that the database can easily be repopulated after it becomes corrupted as a result of repeated testing. It's even more desirable to define large volumes of realistic sample data in a file using loops to avoid having to specify each separate object.

Code to do this should be created in the folder lib/tasks. Here's a possible example of a file sample_data.rake.

namespace :db do
  desc "Fill database with sample data"
  task populate: :environment do
    # Explicit category definitions
    Category.create!(name: "Books", description: "readable items")
    Category.create!(name: "Movies", description: "watchable items")

    # Explicit item definitions
    Item.create!(name: "Catch 22",
                 vendor: "Rodney",
                 category_id: 1,
                 description: "Great anti-war novel by Joseph Heller.",
                 starting_price: 9.99)

    # Repetitive, implicit item definitions
    99.times do |n|
      name  = "Book-#{n+1}"
      vendor = "Vendor-#{n % 10)"
      category_id = n % 2
      Item.create!(name: name,
                   vendor: vendor,
                   category_id: categor_id)
    end
  end
end

To populate the database with this sample data, give the following two commands (the first command empties the database).

rake db:reset
rake db:populate

Of course, it's also possible to use random number (and random word) generation in creating sample data.

The Ruby gem faker is particularly useful for generating random users, creating ramdom, but realistic names. To install the gem faker, give the following command in the app console.

gem install rake

(When installing gems, it may be necessary to add a command "install rake" to the file Gemfile and run the command "bundle install" in the app console.)

Pagination

Of course, we always with to display long lists of item summaries with the list-detail pattern one page at a time. To do this, we must install one of several gems, e.g., will_paginate.

gem install will_paginate

Then, rewrite the definition of the action index (for example), as follows.

def index
  @items = Item.paginate(params[:page], per_page: m)
end

Finally, in the view index.html.erb, add the following after (and/or before) the list of items.

<%= will_paginate @items %>

That's all. Often you can omit the @items from the will_paginate command. There are a few complications that sometimes occur, but these are described in the reference. Remember to style your pagination links nicely.

References

Other patterns

Other common patterns for image management, date/time management and, especially, user management will be described later.