Start with the skeleton built in a previous lesson, call the application module RailsMVC.
- Gemfile: gems, including Rails
- config.ru: rack-based startup script
- script/rails: rails-based startup script
- config/environment.rb: application initialization
- config/boot.rb: application boot
- config/application.rb: application
- config/initializers/secret_token.rb: base cryptographic blob
- public/index.html: hello world
A route connects an HTTP request with a controller. Routes are declared in config/routes.rb.
RailsMVC::Application.routes.draw do
# routes declared here
endDefault RESTful routing connects standard HTTP verbs (GET, PUT, POST and DELETE) with standard resource actions (:index, :show, :new, :edit, :update and :destroy).
Tasks, such as database migrations, are written in Rake. Rake needs a Rakefile, which imports the application's code. Therefore, you have access to all domain models in Rake tasks.
require File.expand_path('../config/application', __FILE__)
require 'rake'
RailsMVC::Application.load_tasksYou can run rake -T to see available tasks.
$ rake -T
rake about # List versions of all Rails frameworks and the environment
...
Application code is located in app, by convention.
The asset pipeline enables authoring JavaScript, CSS and other static artifacts in many ways and compile or package them for the application. For example, in Rails 3.1 the asset pipeline enabled developers to write CoffeeScript instead of raw JavaScript out of the box.
Enable the application pipeline in config/application.rb.
# Enable the asset pipeline
config.assets.enabled = true
Dynamic HTML in Rails is rendered with templates. Templates can be authored in multiple languages and execute Ruby code. Rails uses ERB by default. Another popular templating language is HAML. Add haml-rails to Gemfile and an application layout app/views/layouts/application.html.haml. A layout is a top-level template that defines the page structure, includes stylesheets and javascripts.
!!!
%html
%head
%title
Rails MVC
= stylesheet_link_tag "application"
= javascript_include_tag "application"
%body
= yieldThe stylesheet_link_tag and javascript_include_tag methods are declared in ActionView::Helpers::AssetTagHelper, which is the tip of the asset pipeline. While you can hardcode links in your templates, these functions work with the asset pipeline's concepts and let you add global settings, such as asset host for assets hosted externally on a CDN. It's pretty common for Rails applications to generate assets at build time and deploy these to a web server.
The asset pipeline has the concept of a manifest file which include directives to let rails know which files to include. Create app/assets/javascripts/application.js - one of these manifest files with the following directives.
//= require jquery
//= require jquery_ujs
//= require_tree .
Rails now ships with jQuery. Add jquery-rails to your Gemfile.
Add a stylesheet to app/assets/stylesheets/application.css.scss. The stylesheet is authored in SCSS, which is an extension of CSS that allows you to use variables, nested rules, mixins, inline imports, etc.
#error_explanation {
background-color: #f0f0f0;
h2 {
background-color: #c00;
color: #fff;
}
ul li {
font-size: 12px;
list-style: square;
}
}Since we're using PostgreSQL we need pg in Gemfile.
Rails configuration files are authored in YAML, which executes Ruby code. You could, for example, reference an environment variable with ENV['VARIABLE']. Add config/database.yml for the database configuration. You can reuse blocks of configuration with <<:.
common: &common
adapter: postgresql
username: rails
password: password
host: localhost
port: 5432
development:
<<: *common
database: rails_development
test:
<<: *common
database: rails_test
production:
<<: *common
database: rails_productionIf you run rake -T you'll notice that there are no database tasks. That's because we're only using the action_controller part of Rails. Change require "action_controller/railtie" in config/application.rb to include rails/all.
require 'rails/all'Re-run rake -T and make sure that database tasks are now displayed.
A database-backed application needs a schema. We could create db/schema.rb and populate it with a schema, but the Rails way is to use database migrations. Migrations support creating a new database and upgrading an existing one with built-in database versioning.
Create db/migrate/1_create_things.rb.
class CreateThings < ActiveRecord::Migration
def change
create_table :things do |t|
t.string :name
t.timestamps
end
end
endWe forgot the description. Let's create another migration to add this.
$ rails generate migration add_description_to_things description:string
invoke active_record
create db/migrate/20111207001110_add_description_to_things.rb
This should generate the following time-stamped migration file:
class AddDescriptionToThings < ActiveRecord::Migration
def change
add_column :things, :description, :string
end
endMigrations run incrementally in the order they were created, as determined by the number in the front of their filename.
Create a database and migrate it from empty to version 1.
rake db:create
rake db:migrate
In Rails, ActiveRecord abstracts database access.
Create app/models/thing.rb.
class Thing < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name
endThe validators are a way to ensure presence and uniqueness of fields. The fields are automagically generated by querying the schema on application startup.
All controllers inherit from ActionController::Base. It's a good idea to create an ApplicationController in app/controllers/application_controller.rb that can implement common logic, such as authentication, in the future.
class ApplicationController < ActionController::Base
endAdd app/controllers/things_controller.rb.
class ThingsController < ApplicationController
endRetrieving all things for the index page.
def index
@things = Thing.all
endRetrieve an existing thing by parameter when showing or editing a thing. The params hash has entries that can be accessed both by string and by symbol.
def show
@thing = Thing.find(params[:id])
endCreate a new thing when clicking on new.
def new
@thing = Thing.new
endPersist a new or update an existing thing when submitting a thing from a new or edit page.
def create
@thing = Thing.new(params[:thing])
if @thing.save
redirect_to @thing, notice: 'Thing was successfully created.'
else
render action: "new"
end
end
def update
@thing = Thing.find(params[:id])
if @thing.update_attributes(params[:thing])
redirect_to @thing, notice: 'Thing was successfully updated.'
else
render action: "edit"
end
endDestroy a thing.
def destroy
@thing = Thing.find(params[:id])
@thing.destroy
redirect_to things_url
endErrors are stored automatically via the model being saved, the controller's job is to re-render the view corresponding to the failed action. Create a partial views/shared/_error_msg.html.haml that will display errors.
- if model.errors.any?
.error_explanation
%h3= "#{pluralize(model.errors.count, "error")} saving #{model.class}:"
%ul
- model.errors.full_messages.each do |msg|
%li= msgThe :index, :show, :new, :edit, :update and :destroy methods are called actions. These are the default for RESTful routing.
Add app/views/things/index.html.haml.
%h1 Things
%table
%tr
%th= "Name"
%th
%th
- @things.each do |thing|
%tr
%td= thing.name
%td= link_to 'Edit', edit_thing_path(thing)
%td= link_to 'Destroy', thing, :confirm => "Are you sure?", :method => :delete
= link_to "New Thing", new_thing_pathRather than hardcoding an entire form, let's use a gem called simple_form and make an edit form partial, app/views/things/_form.html.haml.
= simple_form_for @thing do |f|
= render 'shared/error_msg', model: @thing
= f.input :name
= f.button :submitThe form can be used for the new thing in app/views/things/new.html.haml.
%h1 New Thing
= render 'form'
= link_to 'Back', things_pathIt can also be used to edit a thing in app/views/things/edit.html.haml.
%h1= @thing.name
= render 'form'
= link_to 'Back', things_pathFinally, we should display a thing in app/views/things/show.html.haml.
%ul
%li= "Name: #{@thing.name}"
= link_to 'Edit', edit_thing_path(@thing)
|
= link_to 'Back', things_pathEdit config/routes.rb.
RailsMVC::Application.routes.draw do
resources :things
endReview rake routes.
- Add rspec gem in for testing
- Add a set of unit tests for the Thing model that ensure that a Thing can be created, retrieved by id, updated and destroyed (CRUD).
- Add a set of functional tests for your things_controller that ensures Thing's CRUD.
- Add a set of functional view tests for the things view that ensure the view displays a list of things for the
:indexaction, forms for:edit, contents of a thing on:show, etc. - Bonus point for an integration test that runs a complete scenario of creating, showing, editing, updating and destroying a Thing object end-to-end.
- Bonus point for a route test.