Repo - https://github.com/kieranj/getting-started-with-rails
Slides - http://kieranj.github.io/getting-started-with-rails
$ gem install bundler
$ gem install rails
$ rails new gists
$ cd gists
$ bundle exec rails s
open http://localhost:3000
$ 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
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
$ 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
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 ]
$ 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
$ 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
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
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
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
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
<% 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 %>
<%= render partial: 'template_name' # long hand %>
<%= render 'template_name' # short hand %>
<%= render @collection %>
app/views/gists/index.html.erb
table
tbody
<% @gists.each do |gist| %>
tr
td
<%= link_to gist.name, gist %>
td
tr
<% end %>
tbody
table
app/views/gists/index.html.erb
<%= link_to 'New Gist', new_gist_path %>
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 %>
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
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!
app/models/gist.rb
class Gist < ActiveRecord::Base
Languages = %w(Ruby Javascript Scala Go Python Objective-C)
validates :name, presence: true, uniqueness: true
end
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
We only want to show the visible gists
class Gist < ActiveRecord::Base
...
scope :visible, -> { where(visible: true) }
default_scope order(:created_at)
...
end
class GistsController < ApplicationController
...
def index
@gists = Gist.visible
end
end
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 %>
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
$ 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
$ 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
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
https://github.com/kieranj/gist-o-matic
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
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
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