Skip to content

Latest commit

 

History

History
380 lines (276 loc) · 10.3 KB

File metadata and controls

380 lines (276 loc) · 10.3 KB

Live Rails MVC Application from Scratch

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

Routes

A route connects an HTTP request with a controller. Routes are declared in config/routes.rb.

RailsMVC::Application.routes.draw do
  # routes declared here
end

Default RESTful routing connects standard HTTP verbs (GET, PUT, POST and DELETE) with standard resource actions (:index, :show, :new, :edit, :update and :destroy).

Rakefile

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_tasks

You can run rake -T to see available tasks.

$ rake -T
rake about # List versions of all Rails frameworks and the environment
...

Application Folder

Application code is located in app, by convention.

Asset Pipeline

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
    = yield

The 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;
  }
}

Database Configuration

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_production

If 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.

Database Migrations

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
end

We 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
end

Migrations run incrementally in the order they were created, as determined by the number in the front of their filename.

Local Database

Create a database and migrate it from empty to version 1.

rake db:create
rake db:migrate

Data Model

In Rails, ActiveRecord abstracts database access.

Create app/models/thing.rb.

class Thing < ActiveRecord::Base
  validates_presence_of :name
  validates_uniqueness_of :name
end

The validators are a way to ensure presence and uniqueness of fields. The fields are automagically generated by querying the schema on application startup.

Application Controller

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
end

Things Controller

Add app/controllers/things_controller.rb.

class ThingsController < ApplicationController
end

Retrieving all things for the index page.

def index
  @things = Thing.all
end

Retrieve 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])
end

Create a new thing when clicking on new.

def new
  @thing = Thing.new
end

Persist 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
end

Destroy a thing.

def destroy
  @thing = Thing.find(params[:id])
  @thing.destroy
  redirect_to things_url
end

Errors 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= msg

The :index, :show, :new, :edit, :update and :destroy methods are called actions. These are the default for RESTful routing.

Things View

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_path

Rather 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 :submit

The form can be used for the new thing in app/views/things/new.html.haml.

%h1 New Thing

= render 'form'

= link_to 'Back', things_path

It can also be used to edit a thing in app/views/things/edit.html.haml.

%h1= @thing.name

= render 'form'

= link_to 'Back', things_path

Finally, 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_path

Things Routes

Edit config/routes.rb.

RailsMVC::Application.routes.draw do
  resources :things
end

Review rake routes.

Links

Exercise

  • 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 :index action, 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.