On Github kieranj / getting-started-with-rails
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 actionwill 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| %> <p> <%= f.label :name %> <%= f.text_field :name %> </p> <p> <%= f.label :language %> <%= f.select :language, Gist::Languages, prompt: 'Please select...' %> </p> <p> <%= f.label :visible do %> <%= f.check_box :visible %> Visible? <% end %> </p> <p> <%= f.label :code %> <%= f.text_area :code, rows: 20, cols: 20 %> </p> <%= 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 whitelistedare 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
<ul> <% @gist.errors.full_messages.each do |message| %> <li> <%= message %> </li> <% end %> </ul>
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
<code> <%= raw @gist.code %> </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