Getting Started
with Rails

Repo - https://github.com/kieranj/getting-started-with-rails


Slides - http://kieranj.github.io/getting-started-with-rails

Setup


            $ gem install bundler
          

            $ gem install rails
          

            $ rails new gists
					

            $ cd gists
          

            $ bundle exec rails s
          

            open http://localhost:3000
          

Application Struction

Convention over Configuration

  • app
    • assets
    • controllers
    • helpers
    • mailers
    • models
    • views
  • bin
  • config
  • db
    • migrate
  • ...
  • ...
  • lib
    • tasks
  • log
  • public
  • test/spec
  • tmp
  • Gemfile
  • Rakefile

Models

Create a Model


            $ bundle exec rails g model Gist
          

Model names should always be singular

The table name for model will be the plural version of the model

Edit the Migration

db/migrate/YYMMDDHHMMSS_create_gists.rb


            class CreateGists < ActiveRecord::Migration
              def change
                create_table :gists do |t|
                end
              end
            end
          

            class CreateGists < ActiveRecord::Migration
              def change
                create_table :gists do |t|
                  # An "id" column is created by rails
                  # unless you specify otherwise

                  t.string  :name
                  t.text    :code
                  t.string  :language
                  t.boolean :visible, default: false

                  t.timestamps
                  # automatically adds created_at/updated_at datetime
                  # fields which are updated by Rails
                end

                add_index :gists, :name, unique: true
                add_index :gists, :visible
              end
            end
          

Run the migration


            $ bundle exec rake db:migrate
          

This will change the database structure and update config/schema.rb

schema.rb is the authoritive version of your database schema.rb

Routes

config/routes.rb


            Gists::Application.routes.draw do

              resources :gists

              root to: 'gists#index'

            end
          

For any resource that responds to CRUD actions we set the route using "resources"

For one off actions you can match a HTTP verb to the action, eg


            get '/gists/popular' => 'controller#action'
          

For actions that must respond to multiple request types


            match '/gists/search' => 'gists#search', via: [ :get, :post ]
          

Show all routes


            $ bundle exec rake routes
          

            Prefix  Verb    URI Pattern               Controller#Action
             gists  GET     /gists(.:format)          gists#show
                    POST    /gists(.:format)          gists#create
          new_gist  GET     /gists/new(.:format)      gists#new
         edit_gist  GET     /gists/:id/edit(.:format) gists#edit
              gist  GET     /gists/:id(.:format)      gists#show
                    PATCH   /gists/:id(.:format)      gists#update
                    PUT     /gists/:id(.:format)      gists#update
                    DELETE  /gists/:id(.:format)      gists#destroy
              root  GET     /                         gists#index
          

Controllers

Generating a Controller


            $ bundle exec rails g controller Gists
          

Controller names should always be plural

Controller names map to the resource name in your resource, e.g. resources :gists => gists_controller

Controller Actions

By default when you use "resources" in your routes Rails will expect your controller to define 7 common actions


            Prefix  Verb    URI Pattern               Controller#Action
             gists  GET     /gists(.:format)          gists#show
                    POST    /gists(.:format)          gists#create
          new_gist  GET     /gists/new(.:format)      gists#new
         edit_gist  GET     /gists/:id/edit(.:format) gists#edit
              gist  GET     /gists/:id(.:format)      gists#show
                    PATCH   /gists/:id(.:format)      gists#update
                    PUT     /gists/:id(.:format)      gists#update
                    DELETE  /gists/:id(.:format)      gists#destroy
              root  GET     /                         gists#index
          

Add an index action

app/controllers/gists_controller.rb


            class GistsController < ApplicationController

              def index
                @gists = Gist.all
              end

            end
          

Any instance variables in a controller action
will be available inside the view


            open http://localhost:3000/gists
          

We'll fix that in a moment, but first...

ActiveRecord

Query Interface

These accept a either a symbol/hash, e.g.


  Gist.joins(:user).where(users: { email: 'kieran@invisiblelines.com' })
          

or a SQL snippet...


              
  Gist.joins('INNER JOIN "users" ON "users"."id" = "gists"."user_id"').where(['users.email = ?', 'kieran@invisiblelines.com']) 
          

When interpolating variables into SQL use placeholders to have rails automatically escape the input.
This is done automatically when using a hash.

ActiveRecord queries return a ActiveRecord::Relation.

Relations are lazily loaded, so the query is not actually performed until you use the results.

Examples of methods you'd use to trigger your query

  • first
  • last
  • all
  • count
  • find_each
  • select
  • pluck
  • each, map, inject etc...
  • ...

ActiveRecord Examples

The most recent Ruby gist


              Gist.where(language: 'Ruby').order(:created_at).first
            

All gist language counts


              Gist.group(:language).count
            

All gist names as an array


              Gist.distinct.pluck(:name)
            

Try these out in the Rails console


            $ bundle exec rails c
          

Views

ERB Basics


            <% 1 + 1 %>  # Result is not output

            <%= 1 + 1 %> # Result is output

            <%# 1 + 1 %> # Comment
          

Output is escaped by default

To output an unescaped value, use either


            <%== @model.to_json %>
          

            <%= raw @model.to_json %>
          

Layouts

  • app/views/layouts/application.html.erb
  • This is your default layout
  • <%= yield %> - outputs your template into the layout

Partials

  • Reuse common components by splitting them into partials
  • A partial is a template beginning with an underscore
  • To render a partial

            <%= render partial: 'template_name' # long hand %>
          

            <%= render 'template_name' # short hand %>
          

            <%= render @collection %>
          

Templates

app/views/gists/index.html.erb


            table
              tbody
                <% @gists.each do |gist| %>
                  tr
                    td
                      <%= link_to gist.name, gist %>
                    td
                  tr
                <% end %>
              tbody
            table
          

Add a link to create a new Gist

app/views/gists/index.html.erb


            <%= link_to 'New Gist', new_gist_path %>
          

Forms

app/views/gists/new.html.erb


            <%= form_for(@gist) do |f| %>
              

<%= f.label :name %> <%= f.text_field :name %>

<%= f.label :language %> <%= f.select :language, Gist::Languages, prompt: 'Please select...' %>

<%= f.label :visible do %> <%= f.check_box :visible %> Visible? <% end %>

<%= f.label :code %> <%= f.text_area :code, rows: 20, cols: 20 %>

<%= f.submit 'Create' %> <% end %>
  • URL for form is determined by the object and its state
  • We can override the URL using the :url option
  • Any values assigned to object will be populate the form

Wiring up the Controller

app/controllers/gists_controller.rb


              class GistsController < ApplicationController
                ...

                def new
                  @gist = Gist.new
                end
              end
            

Now we need to handle the POST action


app/controllers/gists_controller.rb


              class GistsController < ApplicationController
                ...

                def create
                  @gist = Gist.new(gist_params)
                  if @gist.save
                    redirect_to gists_path, notice: 'Gist successfully created'
                  else
                    render action: :new
                  end
                end

                private

                  # We need to specify the form parameters we will allow
                  #
                  def gist_params
                    params.require(:gist).permit(:name, :visible, :language, :code)
                  end
              end
            

Strong Parameters

Parameters that have not been whitelisted
are ignored by ActiveModel


To whitelist parameters


            params.require(:gist).allow(:name)
          

            params.require(:gist).allow(:name, language: [:name])
          

            params.require(:gist).permit!
          

Validations

Available Validations

app/models/gist.rb


            class Gist < ActiveRecord::Base

              Languages = %w(Ruby Javascript Scala Go Python Objective-C)
              
              validates :name, presence: true, uniqueness: true

            end
          

Displaying Errors

Validation errors are available in the
"errors" object on the model

"object.errors.full_messages" is an array of all error messages


app/views/gists/new.html.erb


              
    <% @gist.errors.full_messages.each do |message| %>
  • <%= message %>
  • <% end %>

This should really be refactored into a helper

Scopes

  • Must be a callable object, e.g. Proc/lambda, or defined as a class method
  • Can be made up of any number of conditions
  • Are chainable
  • default_scope can be overridden using
    "unscoped" or "reorder(:column_name)"

We only want to show the visible gists


            class Gist < ActiveRecord::Base

              ...

              scope :visible, -> { where(visible: true) }

              default_scope order(:created_at)

              ...
            end
          

Update the controller


            class GistsController < ApplicationController
              ...

              def index
                @gists = Gist.visible
              end
            end
          

Showing the content

app/controllers/gists_controller.rb


            class GistsController < ApplicationController

              def show
                @gist = Gist.find(params[:id])
              end

            end
          

app/views/show.html.erb


            
              <%= raw @gist.code %>
            

            <%= link_to 'Edit', edit_gist_path(@gist) %>
            <%= link_to 'Destroy', gist_path(@gist), method: :delete %>
          

Associations

Association Types


            has_one    :owner # one to one, foreign key on associated object

            belongs_to :user  # one to one, has the foreign key

            has_many   :gists # one to many

            has_many   :categories, through: :gist_categories
            # many to many, join table with attributes

            has_and_belongs_to_many :categories
            # many to many, rarely used nowadays, prefer has_many :through
          

Create a User


            $ bundle exec rails g resource User
          

db/migrate/YYMMDDHHMMSS_create_users.rb


              class CreateUsers < ActiveRecord::Migration
                create_table :users do |t|
                  t.string :email
                  t.string :password_digest
                  t.timestamps
                end

                add_index :users, :email, unique: true
              end
            

Add the foreign key


            $ bundle exec rails g migration add_user_id_to_gists
          

db/migrate/YYMMDDHHMMSS_add_user_id_to_gists.rb


              class AddUserIdToGists < ActiveRecord::Migration
                def change
                  add_column :gists, :user_id, :integer

                  add_index :gists, :user_id
                end
              end
            

Run the migration


              $ bundle exec rake db:migrate
            

Add the Associations

app/models/user.rb


              class User < ActiveRecord::Base

                validates :email, presence: true, uniqueness: true

                has_many :gists, dependent: :destroy

                has_secure_password 
                # for implementing simple authentication
                # ensure gemfile contains bcrypt-ruby

                def email=(string)
                  super(string.try(:downcase))
                end

              end
            

app/models/gist.rb


            class Gist < ActiveRecord::Base

              Languages = %w(Ruby Javascript Scala Go Python Objective-C)

              validates :name, presence: true, uniqueness: true

              belongs_to :user

            end
          

Exercises

  • Handle edit/update/destroy actions for gists
  • Add a controller/views/route for signing up users
  • Add a controller/views/route for authenticating users
  • Only show gists for a particular user

The end

Thank You

More?

https://github.com/kieranj/gist-o-matic

Authentication


            class SessionsController < ApplicationController

              def create
                user = User.where(email: params[:session][:email]).first
                if user && user.authenticate(params[:session][:password])
                  session[:user_id] = user.id
                  redirect_to gists_path
                else
                  redirect_to :back
                end
              end

              def destroy
                session[:user_id] = nil
                redirect_to root_path
              end

            end
          

Authentication Helpers


            class ApplicationController < ActionController::Base

              private
                
                def current_user
                  @current_user ||= User.find(session[:user_id]) if session[:user_id]
                end

                def authenticate
                  redirect_to(new_session_path) unless signed_in?
                end

                def signed_in?
                  !!current_user
                end

                helper_method :current_user, :signed_in?
            end
          

Filter


            class GistsController < ApplicationController

              # require a signed in user to create a gist
              before_action :authenticate, except: [ :index, :show ]

              def create
                # use the association to build the resource
                @gist = current_user.gists.build(gist_params)
                ...
              end
            end