Rails MVC
1
Model-View-Controller
Trygve Reenskaug. 1978
● Separation of business logic
● Reusing of code
● Separation of responsibility
● Flexible application
BENEFITS
● Doesn’t have a strict implementation
● No one place for busines logic
● No one place for validations
● No one place for views
PROPERTIES
Rails MVC
in the context
of user
MVC is an Architectural Pattern
8
My Banana
Stuart, give me
the banana
Ostap, are
you here?
I’m coming
put on a suit
Farewell !!!
Rails MVC
in the context
of components
10
Router
Controller ModelResponse
Views DataBase
Rails MVC
in the context
of Rails libraries
12
ROUTES
13
resource :profile, only: %i[show edit update], controller: :profile
14
RESOURCE
1 Rails.application.routes.draw do
2 root 'home#index'
3
4 resources :events, only: %i[index show] do
5 resources :visit_requests, only: %i[show create destroy]
6 end
15
RESOURCES
CUSTOM ROUTES WITHIN RESOURCES
1 resources :orders do
2 collection do
3 post :search
4 end
5 member do
6 post :accept
7 post :reject
8 post :ship
9 get :despatch_note
10 end
11 end
16
get '/:page_url', to: 'pages#show'
authenticate :user, ->(u) { u.admin? } do
namespace :admin do
get '/', to: 'home#index'
resources :accounts
17
CUSTOM ROUTES, NAMESPACE
1 # api
2 namespace :api, path: '/', constraints: { subdomain: 'api' }, defaults: { format: :json } do
3 namespace :v1 do
4 post 'sign_up' => 'registrations#create'
5 post 'sign_in' => 'sessions#create'
6 delete 'sign_out' => 'sessions#destroy'
7 end
8 end
18
NAMESPACES
api_v1_sign_up POST /v1/sign_up(.:format) api/v1/registrations#create
{:subdomain=>"api", :format=>:json}
api_v1_sign_in POST /v1/sign_in(.:format) api/v1/sessions#create
{:subdomain=>"api", :format=>:json}
api_v1_sign_out DELETE /v1/sign_out(.:format) api/v1/sessions#destroy
{:subdomain=>"api", :format=>:json}
1 # api
2 scope :api, constraints: { subdomain: 'api' }, module: :api, defaults: { format: :json }, as: :api do
3 scope '/v1', module: :v1, as: :v1 do
4 post 'sign_up' => 'registrations#create'
5 post 'sign_in' => 'sessions#create'
6 delete 'sign_out' => 'sessions#destroy'
7 end
8 end
19
SCOPES
api_v1_sign_up POST /v1/sign_up(.:format) api/v1/registrations#create
{:subdomain=>"api", :format=>:json}
api_v1_sign_in POST /v1/sign_in(.:format) api/v1/sessions#create
{:subdomain=>"api", :format=>:json}
api_v1_sign_out DELETE /v1/sign_out(.:format) api/v1/sessions#destroy
{:subdomain=>"api", :format=>:json}
20
Wait!
Aren’t namespaces and scopes essentially the same?
1 # api
2 scope :api do
3 scope :v1 do
4 post 'sign_up' => 'registrations#create'
5 post 'sign_in' => 'sessions#create'
6 delete 'sign_out' => 'sessions#destroy'
7 end
8 end
21
SCOPES WITHOUT ANY OPTIONS
sign_up POST /sign_up(.:format) registrations#create
sign_in POST /sign_in(.:format) sessions#create
sign_out DELETE /sign_out(.:format) sessions#destroy
22
NAMESPACE vs SCOPE
Namespace Scope
By default, adds the name of the
namespace to the name of the path,
prefixes actual request path, and expects
the controller to belong to appropriately
named module.
No defaults. All options are explicit.
A shortcut method when you need to
quickly nest a set of routes and controllers
under some name.
A powerful, customizable method to apply
defaults to a group of routes.
1 namespace :admin do
2 root "admin#index"
3 end
4
5 root "home#index"
23
MULTIPLE ROOT ROUTES
1 constraints(id: /[A-Z][A-Z][0-9]+/) do
2 resources :photos
3 resources :accounts
4 end
24
CONSTRAINTS
CONTROLLERS
25
1 def create
2 @product = Product.new(safe_params)
3 if @product.save
4 redirect_to :products, flash: { notice: t('shopr.products.create_notice') }
5 else
6 render action: 'new'
7 end
8 end
9
10 def update
11 if @product.update(safe_params)
12 redirect_to [:edit, @product], flash: { notice: t('products.update_notice') }
13 else
14 render action: 'edit'
15 end
16 end
26
DEFAULT ACTIONS
1 class ApplicationController < ActionController::Base
2 protect_from_forgery with: :exception
3 before_action :auth, only: :admin
4 helper_method :admin?, :about_page, :contacts_page
6 delegate :admin?, to: :current_user, allow_nil: true
7 skip_authorization_check
8 skip_before_action :authenticate_user!
27
HELPERS, BEFORE ACTIONS, ...
1 def create
2 @protocol = Protocol.create(protocol_params)
3 end
4
5 private
6
7 def protocol_params
8 params.require(:protocol).permit(
9 :competition_id,
10 :first_name,
11 :last_name,
12 :total_result,
13 :place
14 )
15 end
28
PERMITTED PARAMS
1 def create
2 @product = Product.new(safe_params)
3 if @product.save
4 redirect_to :products, flash: { notice: t('shopr.products.create_notice') }
5 else
6 render action: 'new'
7 end
8 end
29
REDIRECT VS RENDER
render :edit
render action: :edit
render "edit"
render "edit.html.erb"
render action: "edit"
render action: "edit.html.erb"
render "books/edit"
render "books/edit.html.erb"
30
RENDER
render template: "books/edit"
render template: "books/edit.html.erb"
render "/path/to/rails/app/views/books/edit"
render "/path/to/rails/app/views/books/edit.html.erb"
render file: "/path/to/rails/app/views/books/edit"
render file: "/path/to/rails/app/views/books/edit.html.erb"
1 def create
2 Shopr::Order.transaction do
3 @order = Shopr::Order.new(safe_params)
4 @order.status = 'confirming'
5 if safe_params[:customer_id]
6 @customer = Shopr::Customer.find safe_params[:customer_id]
7 @order.first_name = @customer.first_name
8 @order.last_name = @customer.last_name
9 @order.company = @customer.company
10 @order.email_address = @customer.email
11 @order.phone_number = @customer.phone
12 if @customer.addresses.billing.present?
13 billing = @customer.addresses.billing.first
14 @order.billing_address1 = billing.address1
15 @order.billing_address2 = billing.address2
16 @order.billing_address3 = billing.address3
17 @order.billing_address4 = billing.address4
18 @order.billing_postcode = billing.postcode
19 @order.billing_country_id = billing.country_id
31
FAT CONTROLLERS
1 class VisitRequestsController < ApplicationController
2 before_action :authenticate_user!, only: %i[create destroy]
3
4 def show
5 VisitRequest::FinalConfirmation.call(visit_request, params)
6 flash_success(visit_request.status) and default_redirect
7 end
8
9 def create
10 VisitRequest::Create.call(current_user, event)
11 flash_success and default_redirect
12 end
13
14 def destroy
15 visit_request.destroy
16 flash_success and default_redirect
17 end
32
THIN CONTROLLERS
MODELS
33
1 class Goal < ApplicationRecord
2 has_many :donations, dependent: :destroy
3
4 validates :title, presence: true
5 validates :amount, presence: true, numericality: true
6
7 def achieved?
8 donations_total >= amount
9 end
10
11 def donations_total
12 donations.sum(:amount)
13 end
14 end
34
MODELS
class Goal < ApplicationRecord
has_many :donations, dependent: :destroy
end
class Donation < ApplicationRecord
belongs_to :goal
end
35
ONE TO MANY/ONE RELATIONS
Goal
id:integer
title:string
Donation
id:integer
goal_id:integer
title:string
...
...
36
MANY TO MANY RELATIONS
Goal
id:integer
title:string
Donation
id:integer
title:string
... ...
class Goal < ApplicationRecord
has_and_belongs_to_many :donations
end
class Donation < ApplicationRecord
has_and_belongs_to_many :goal
end
donations_goals
goal_id:integer
donation_id:integer
37
ASSOCIATION RELATIONS
Goal
id:integer
title:string
Donation
id:integer
goal_id:integer
...
...
class Goal < ApplicationRecord
has_many :donations
has_many :users, through: :donations
end
class Donation < ApplicationRecord
belongs_to :goal
belongs_to :user
end
class User < ApplicationRecord
has_many :donations
end
User
id:integer
email:string
...
user_id:integer
38
POLYMORPHIC RELATIONS
User
id:integer
email:string
Company
id:integer
name:string
... ...
Address
id:integer
address:string
addressable_id:integer
addressable_type:string
class Address < ApplicationRecord
belongs_to :addressable, polymorphic: true
end
class User < ApplicationRecord
has_many :address, as: :assressable
end
class Company < ApplicationRecord
has_many :address, as: :assressable
end
1 class Goal < ApplicationRecord
2 validates :title, presence: true
3 end
4
5 goal = Goal.create(title: “Buying a motocycle”).valid? # => true
6 goal = Goal.create(title: nil).valid? # => false
7 goal.errors.messages # => {title:["can't be blank"]}
39
VALIDATES
1 class Goal < ApplicationRecord
2 validates_associated :donations #(only in one of related model)
3 validates_with GoalValidator #class GoalValidator < ActiveModel::Validator
4 validate :expiration_date_cannot_be_in_the_past
5
6 def expiration_date_cannot_be_in_the_past
7 if expiration_date.present? && expiration_date < Date.today
8 errors.add(:expiration_date, "can't be in the past")
9 end
10 end
11 end
40
VALIDATES
VIEWS
41
1 <h2> <%= t 'goals.singular'> </h1>
2
3 < div class="col-md-12" >
4 <%= goal.title >
5 < /div>
6 < div class="col-md-12" >
7 <%= goal.description >
8 < /div>
9 < div class="col-md-12" >
10 <%= "Amount to reach: #{goal.amount}" >
11 < /div>
12 < div class="col-md-12" >
13 <%= "Current sum: #{goal.donations_total}" >
14 < /div>
42
TEMPLATES
1 <h2> <%= t 'goals.singular'> </h1>
2
3 < div class="col-md-12" >
4 <%= goal.title >
5 < /div>
6 < div class="col-md-12" >
7 <%= goal.description >
8 < /div>
9 < div class="col-md-12" >
10 <%= "Amount to reach: #{goal.amount}" >
11 < /div>
12 < div class="col-md-12" >
13 <%= "Current sum: #{goal.donations_total}" >
14 < /div>
43
.html.erb (ERB - Embedded Ruby)
1 h2 = t 'goals.singular'
2
3 .col-md-12
4 = goal.title
5 .col-md-12
6 = goal.description
7 .col-md-12
8 = "Amount to reach: #{goal.amount}"
9 .col-md-12
10 = "Current sum: #{goal.donations_total}"
11 .col-md-12
12 - unless goal.achieved?
13 = render 'shared/donations/form', path: donate_goal_path(goal)
44
.html.slim
1 $('#notifications_list').html(
2 "<%= escape_javascript(render('notifications/list', notifications: @notifications)) %>"
3 );
4
5 $('.container-fluid .flash-msg').html(
6 "<%= escape_javascript(render 'layouts/alerts') %>"
7 );
8
9 $("#send-notification-modal").modal("hide");
45
.js.erb
1 json.extract! ticket, :id,
:uid,
:manager_id,
:customer_name,
:customer_email,
:title,
:body,
:status_id
2 json.url ticket_url(ticket, format: :json)
46
.json.jbuilder
new.html.slim
1 = render 'shared/donations/form',
path: donate_goal_path(goal)
edit.html.slim
1 = render 'shared/donations/form',
path: donate_goal_path(goal)
47
PARTIALS
_form.html.slim
= simple_form_for :credit_card, url: path do |f|
.form-inputs
.row
.col-md-6
= f.input :number, as: :string
= f.input :cvc, as: :string
= f.input :exp_month, as: :string
= f.input :exp_year, as: :string
1 <h1>Listing Books</h1>
2 <table>
3 <tr>
4 <th>Title</th>
5 <th>Summary</th>
6 <th></th>
7 <th></th>
8 <th></th>
9 </tr>
10 <% @books.each do |book| %>
11 <tr>
12 <td><%= book.title %></td>
13 <td><%= book.content %></td>
14 <td><%= link_to "Show", book %></td>
15 <td><%= link_to "Edit", edit_book_path(book) %></td>
16 <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
17 </tr>
18 <% end %>
19 </table>
48
PARTIALS. MAGIC
/views/books/index.html.erb
1 <h1>Listing Books</h1>
2 <table>
3 <tr>
4 <th>Title</th>
5 <th>Summary</th>
6 <th></th>
7 <th></th>
8 <th></th>
9 </tr>
10 <%= render @books %>
11 </table> 49
PARTIALS. MAGIC
/views/books/_book.html.erb
1 <tr>
2 <td><%= book.title %></td>
3 <td><%= book.content %></td>
4 <td><%= link_to "Show", book %></td>
5 <td><%= link_to "Edit", edit_book_path(book) %></td>
16 <td><%= link_to "Remove", book, method: :delete, data:
{ confirm: "Are you sure?" } %></td>
17 </tr>
50
LAYOUTS
class HomeController < ApplicationController
def index
render 'events/show'
end
end
51
LAYOUTS
class TalksController < ApplicationController
helper_method :talks, :talk, :tags
private
def talks
scope = Talk.published.includes(:event).order('events.finished_at desc')
@talks ||= params[:tag] ? scope.tagged_with(params[:tag]) : scope
@talks.page(params[:page]).per(12)
end
end
class PagesController < ApplicationController
helper_method :page
def show
page ? render(:show) : not_found
end
private
def page
@page ||= Page.find_by(url: params[:page_url])
end
end
class AuthController < ActionController::Base
layout "devise"
end
doctype html
html
head
title
= content_for?(:title) ? yield(:title) : t('default_title')
= stylesheet_link_tag 'app/application'
meta name="theme-color" content="#ffffff"
== render 'layouts/ga/head'
body class="#{yield(:main_body_class)}"
= yield(:before_header)
== render 'layouts/app/flash'
== render 'layouts/app/header'
main
= yield
52
application.slim vs admin.slim
doctype html
html
head
title
= content_for?(:title) ? yield(:title) : t('default_admin_title')
= stylesheet_link_tag 'admin/application'
meta name="viewport" content="width=device-width,
initial-scale=1, shrink-to-fit=no"
body
.ui.sidebar.inverted.vertical.menu
== render 'layouts/admin/navigation'
.pusher
.ui.container#container
== render 'layouts/admin/header'
= yield
== render 'layouts/admin/footer'
views/events/index.slim
...
tbody
- events.each do |event|
tr
th = event.id
td = resource_link(event)
td = format_timestamp(event.started_at)
td = format_timestamp(event.finished_at)
53
HELPERS
helpers/...
module TalksHelper
def talk_link(talk, text = "", options = {})
link_to text, polymorphic_path(talk), options
end
end
module ApplicationHelper
def format_timestamp(timestamp, time: true, delimiter: '-')
return unless timestamp
formatted_date = timestamp.strftime('%Y %b %d')
formatted_time = timestamp.strftime('%H:%M')
return formatted_date if !time
"#{formatted_date} #{delimiter} #{formatted_time}"
end
● Fat model and Skinny controller
● Business logic should always be in the
model
● The view should have minimal code
● Use helpers!
● Use models
● DRY (Don't Repeat Yourself)
54
BEST
PRACTICES
https://github.com/rails/rails
http://api.rubyonrails.org/
http://guides.rubyonrails.org/
https://github.com/bbatsov/rails-style-guide
55
RESOURCES

Rails MVC by Sergiy Koshovyi

  • 1.
  • 3.
  • 5.
    ● Separation ofbusiness logic ● Reusing of code ● Separation of responsibility ● Flexible application BENEFITS
  • 6.
    ● Doesn’t havea strict implementation ● No one place for busines logic ● No one place for validations ● No one place for views PROPERTIES
  • 7.
    Rails MVC in thecontext of user
  • 8.
    MVC is anArchitectural Pattern 8 My Banana Stuart, give me the banana Ostap, are you here? I’m coming put on a suit Farewell !!!
  • 9.
    Rails MVC in thecontext of components
  • 10.
  • 11.
    Rails MVC in thecontext of Rails libraries
  • 12.
  • 13.
  • 14.
    resource :profile, only:%i[show edit update], controller: :profile 14 RESOURCE
  • 15.
    1 Rails.application.routes.draw do 2root 'home#index' 3 4 resources :events, only: %i[index show] do 5 resources :visit_requests, only: %i[show create destroy] 6 end 15 RESOURCES
  • 16.
    CUSTOM ROUTES WITHINRESOURCES 1 resources :orders do 2 collection do 3 post :search 4 end 5 member do 6 post :accept 7 post :reject 8 post :ship 9 get :despatch_note 10 end 11 end 16
  • 17.
    get '/:page_url', to:'pages#show' authenticate :user, ->(u) { u.admin? } do namespace :admin do get '/', to: 'home#index' resources :accounts 17 CUSTOM ROUTES, NAMESPACE
  • 18.
    1 # api 2namespace :api, path: '/', constraints: { subdomain: 'api' }, defaults: { format: :json } do 3 namespace :v1 do 4 post 'sign_up' => 'registrations#create' 5 post 'sign_in' => 'sessions#create' 6 delete 'sign_out' => 'sessions#destroy' 7 end 8 end 18 NAMESPACES api_v1_sign_up POST /v1/sign_up(.:format) api/v1/registrations#create {:subdomain=>"api", :format=>:json} api_v1_sign_in POST /v1/sign_in(.:format) api/v1/sessions#create {:subdomain=>"api", :format=>:json} api_v1_sign_out DELETE /v1/sign_out(.:format) api/v1/sessions#destroy {:subdomain=>"api", :format=>:json}
  • 19.
    1 # api 2scope :api, constraints: { subdomain: 'api' }, module: :api, defaults: { format: :json }, as: :api do 3 scope '/v1', module: :v1, as: :v1 do 4 post 'sign_up' => 'registrations#create' 5 post 'sign_in' => 'sessions#create' 6 delete 'sign_out' => 'sessions#destroy' 7 end 8 end 19 SCOPES api_v1_sign_up POST /v1/sign_up(.:format) api/v1/registrations#create {:subdomain=>"api", :format=>:json} api_v1_sign_in POST /v1/sign_in(.:format) api/v1/sessions#create {:subdomain=>"api", :format=>:json} api_v1_sign_out DELETE /v1/sign_out(.:format) api/v1/sessions#destroy {:subdomain=>"api", :format=>:json}
  • 20.
    20 Wait! Aren’t namespaces andscopes essentially the same?
  • 21.
    1 # api 2scope :api do 3 scope :v1 do 4 post 'sign_up' => 'registrations#create' 5 post 'sign_in' => 'sessions#create' 6 delete 'sign_out' => 'sessions#destroy' 7 end 8 end 21 SCOPES WITHOUT ANY OPTIONS sign_up POST /sign_up(.:format) registrations#create sign_in POST /sign_in(.:format) sessions#create sign_out DELETE /sign_out(.:format) sessions#destroy
  • 22.
    22 NAMESPACE vs SCOPE NamespaceScope By default, adds the name of the namespace to the name of the path, prefixes actual request path, and expects the controller to belong to appropriately named module. No defaults. All options are explicit. A shortcut method when you need to quickly nest a set of routes and controllers under some name. A powerful, customizable method to apply defaults to a group of routes.
  • 23.
    1 namespace :admindo 2 root "admin#index" 3 end 4 5 root "home#index" 23 MULTIPLE ROOT ROUTES
  • 24.
    1 constraints(id: /[A-Z][A-Z][0-9]+/)do 2 resources :photos 3 resources :accounts 4 end 24 CONSTRAINTS
  • 25.
  • 26.
    1 def create 2@product = Product.new(safe_params) 3 if @product.save 4 redirect_to :products, flash: { notice: t('shopr.products.create_notice') } 5 else 6 render action: 'new' 7 end 8 end 9 10 def update 11 if @product.update(safe_params) 12 redirect_to [:edit, @product], flash: { notice: t('products.update_notice') } 13 else 14 render action: 'edit' 15 end 16 end 26 DEFAULT ACTIONS
  • 27.
    1 class ApplicationController< ActionController::Base 2 protect_from_forgery with: :exception 3 before_action :auth, only: :admin 4 helper_method :admin?, :about_page, :contacts_page 6 delegate :admin?, to: :current_user, allow_nil: true 7 skip_authorization_check 8 skip_before_action :authenticate_user! 27 HELPERS, BEFORE ACTIONS, ...
  • 28.
    1 def create 2@protocol = Protocol.create(protocol_params) 3 end 4 5 private 6 7 def protocol_params 8 params.require(:protocol).permit( 9 :competition_id, 10 :first_name, 11 :last_name, 12 :total_result, 13 :place 14 ) 15 end 28 PERMITTED PARAMS
  • 29.
    1 def create 2@product = Product.new(safe_params) 3 if @product.save 4 redirect_to :products, flash: { notice: t('shopr.products.create_notice') } 5 else 6 render action: 'new' 7 end 8 end 29 REDIRECT VS RENDER
  • 30.
    render :edit render action::edit render "edit" render "edit.html.erb" render action: "edit" render action: "edit.html.erb" render "books/edit" render "books/edit.html.erb" 30 RENDER render template: "books/edit" render template: "books/edit.html.erb" render "/path/to/rails/app/views/books/edit" render "/path/to/rails/app/views/books/edit.html.erb" render file: "/path/to/rails/app/views/books/edit" render file: "/path/to/rails/app/views/books/edit.html.erb"
  • 31.
    1 def create 2Shopr::Order.transaction do 3 @order = Shopr::Order.new(safe_params) 4 @order.status = 'confirming' 5 if safe_params[:customer_id] 6 @customer = Shopr::Customer.find safe_params[:customer_id] 7 @order.first_name = @customer.first_name 8 @order.last_name = @customer.last_name 9 @order.company = @customer.company 10 @order.email_address = @customer.email 11 @order.phone_number = @customer.phone 12 if @customer.addresses.billing.present? 13 billing = @customer.addresses.billing.first 14 @order.billing_address1 = billing.address1 15 @order.billing_address2 = billing.address2 16 @order.billing_address3 = billing.address3 17 @order.billing_address4 = billing.address4 18 @order.billing_postcode = billing.postcode 19 @order.billing_country_id = billing.country_id 31 FAT CONTROLLERS
  • 32.
    1 class VisitRequestsController< ApplicationController 2 before_action :authenticate_user!, only: %i[create destroy] 3 4 def show 5 VisitRequest::FinalConfirmation.call(visit_request, params) 6 flash_success(visit_request.status) and default_redirect 7 end 8 9 def create 10 VisitRequest::Create.call(current_user, event) 11 flash_success and default_redirect 12 end 13 14 def destroy 15 visit_request.destroy 16 flash_success and default_redirect 17 end 32 THIN CONTROLLERS
  • 33.
  • 34.
    1 class Goal< ApplicationRecord 2 has_many :donations, dependent: :destroy 3 4 validates :title, presence: true 5 validates :amount, presence: true, numericality: true 6 7 def achieved? 8 donations_total >= amount 9 end 10 11 def donations_total 12 donations.sum(:amount) 13 end 14 end 34 MODELS
  • 35.
    class Goal <ApplicationRecord has_many :donations, dependent: :destroy end class Donation < ApplicationRecord belongs_to :goal end 35 ONE TO MANY/ONE RELATIONS Goal id:integer title:string Donation id:integer goal_id:integer title:string ... ...
  • 36.
    36 MANY TO MANYRELATIONS Goal id:integer title:string Donation id:integer title:string ... ... class Goal < ApplicationRecord has_and_belongs_to_many :donations end class Donation < ApplicationRecord has_and_belongs_to_many :goal end donations_goals goal_id:integer donation_id:integer
  • 37.
    37 ASSOCIATION RELATIONS Goal id:integer title:string Donation id:integer goal_id:integer ... ... class Goal< ApplicationRecord has_many :donations has_many :users, through: :donations end class Donation < ApplicationRecord belongs_to :goal belongs_to :user end class User < ApplicationRecord has_many :donations end User id:integer email:string ... user_id:integer
  • 38.
    38 POLYMORPHIC RELATIONS User id:integer email:string Company id:integer name:string ... ... Address id:integer address:string addressable_id:integer addressable_type:string classAddress < ApplicationRecord belongs_to :addressable, polymorphic: true end class User < ApplicationRecord has_many :address, as: :assressable end class Company < ApplicationRecord has_many :address, as: :assressable end
  • 39.
    1 class Goal< ApplicationRecord 2 validates :title, presence: true 3 end 4 5 goal = Goal.create(title: “Buying a motocycle”).valid? # => true 6 goal = Goal.create(title: nil).valid? # => false 7 goal.errors.messages # => {title:["can't be blank"]} 39 VALIDATES
  • 40.
    1 class Goal< ApplicationRecord 2 validates_associated :donations #(only in one of related model) 3 validates_with GoalValidator #class GoalValidator < ActiveModel::Validator 4 validate :expiration_date_cannot_be_in_the_past 5 6 def expiration_date_cannot_be_in_the_past 7 if expiration_date.present? && expiration_date < Date.today 8 errors.add(:expiration_date, "can't be in the past") 9 end 10 end 11 end 40 VALIDATES
  • 41.
  • 42.
    1 <h2> <%=t 'goals.singular'> </h1> 2 3 < div class="col-md-12" > 4 <%= goal.title > 5 < /div> 6 < div class="col-md-12" > 7 <%= goal.description > 8 < /div> 9 < div class="col-md-12" > 10 <%= "Amount to reach: #{goal.amount}" > 11 < /div> 12 < div class="col-md-12" > 13 <%= "Current sum: #{goal.donations_total}" > 14 < /div> 42 TEMPLATES
  • 43.
    1 <h2> <%=t 'goals.singular'> </h1> 2 3 < div class="col-md-12" > 4 <%= goal.title > 5 < /div> 6 < div class="col-md-12" > 7 <%= goal.description > 8 < /div> 9 < div class="col-md-12" > 10 <%= "Amount to reach: #{goal.amount}" > 11 < /div> 12 < div class="col-md-12" > 13 <%= "Current sum: #{goal.donations_total}" > 14 < /div> 43 .html.erb (ERB - Embedded Ruby)
  • 44.
    1 h2 =t 'goals.singular' 2 3 .col-md-12 4 = goal.title 5 .col-md-12 6 = goal.description 7 .col-md-12 8 = "Amount to reach: #{goal.amount}" 9 .col-md-12 10 = "Current sum: #{goal.donations_total}" 11 .col-md-12 12 - unless goal.achieved? 13 = render 'shared/donations/form', path: donate_goal_path(goal) 44 .html.slim
  • 45.
    1 $('#notifications_list').html( 2 "<%=escape_javascript(render('notifications/list', notifications: @notifications)) %>" 3 ); 4 5 $('.container-fluid .flash-msg').html( 6 "<%= escape_javascript(render 'layouts/alerts') %>" 7 ); 8 9 $("#send-notification-modal").modal("hide"); 45 .js.erb
  • 46.
    1 json.extract! ticket,:id, :uid, :manager_id, :customer_name, :customer_email, :title, :body, :status_id 2 json.url ticket_url(ticket, format: :json) 46 .json.jbuilder
  • 47.
    new.html.slim 1 = render'shared/donations/form', path: donate_goal_path(goal) edit.html.slim 1 = render 'shared/donations/form', path: donate_goal_path(goal) 47 PARTIALS _form.html.slim = simple_form_for :credit_card, url: path do |f| .form-inputs .row .col-md-6 = f.input :number, as: :string = f.input :cvc, as: :string = f.input :exp_month, as: :string = f.input :exp_year, as: :string
  • 48.
    1 <h1>Listing Books</h1> 2<table> 3 <tr> 4 <th>Title</th> 5 <th>Summary</th> 6 <th></th> 7 <th></th> 8 <th></th> 9 </tr> 10 <% @books.each do |book| %> 11 <tr> 12 <td><%= book.title %></td> 13 <td><%= book.content %></td> 14 <td><%= link_to "Show", book %></td> 15 <td><%= link_to "Edit", edit_book_path(book) %></td> 16 <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td> 17 </tr> 18 <% end %> 19 </table> 48 PARTIALS. MAGIC
  • 49.
    /views/books/index.html.erb 1 <h1>Listing Books</h1> 2<table> 3 <tr> 4 <th>Title</th> 5 <th>Summary</th> 6 <th></th> 7 <th></th> 8 <th></th> 9 </tr> 10 <%= render @books %> 11 </table> 49 PARTIALS. MAGIC /views/books/_book.html.erb 1 <tr> 2 <td><%= book.title %></td> 3 <td><%= book.content %></td> 4 <td><%= link_to "Show", book %></td> 5 <td><%= link_to "Edit", edit_book_path(book) %></td> 16 <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td> 17 </tr>
  • 50.
  • 51.
    class HomeController <ApplicationController def index render 'events/show' end end 51 LAYOUTS class TalksController < ApplicationController helper_method :talks, :talk, :tags private def talks scope = Talk.published.includes(:event).order('events.finished_at desc') @talks ||= params[:tag] ? scope.tagged_with(params[:tag]) : scope @talks.page(params[:page]).per(12) end end class PagesController < ApplicationController helper_method :page def show page ? render(:show) : not_found end private def page @page ||= Page.find_by(url: params[:page_url]) end end class AuthController < ActionController::Base layout "devise" end
  • 52.
    doctype html html head title = content_for?(:title)? yield(:title) : t('default_title') = stylesheet_link_tag 'app/application' meta name="theme-color" content="#ffffff" == render 'layouts/ga/head' body class="#{yield(:main_body_class)}" = yield(:before_header) == render 'layouts/app/flash' == render 'layouts/app/header' main = yield 52 application.slim vs admin.slim doctype html html head title = content_for?(:title) ? yield(:title) : t('default_admin_title') = stylesheet_link_tag 'admin/application' meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" body .ui.sidebar.inverted.vertical.menu == render 'layouts/admin/navigation' .pusher .ui.container#container == render 'layouts/admin/header' = yield == render 'layouts/admin/footer'
  • 53.
    views/events/index.slim ... tbody - events.each do|event| tr th = event.id td = resource_link(event) td = format_timestamp(event.started_at) td = format_timestamp(event.finished_at) 53 HELPERS helpers/... module TalksHelper def talk_link(talk, text = "", options = {}) link_to text, polymorphic_path(talk), options end end module ApplicationHelper def format_timestamp(timestamp, time: true, delimiter: '-') return unless timestamp formatted_date = timestamp.strftime('%Y %b %d') formatted_time = timestamp.strftime('%H:%M') return formatted_date if !time "#{formatted_date} #{delimiter} #{formatted_time}" end
  • 54.
    ● Fat modeland Skinny controller ● Business logic should always be in the model ● The view should have minimal code ● Use helpers! ● Use models ● DRY (Don't Repeat Yourself) 54 BEST PRACTICES
  • 55.