Ror lab. season 2
 - the 6th round -


Active Record
Validations

 September 15th, 2012

   Hyoseong Choi
     ROR Lab.
Contents
      Validations                         Callbacks
•   The Object Life Cycle          •   Callbacks Overview
•   Validations Overview           •   Available Callbacks
•   Validation Helpers             •   Running Callbacks
•   Common Validation              •   Skipping Callbacks
    Options                        •   Halting Execution
•   Conditional Validation         •   Relational Callbacks
•   Custom Validations             •   Conditional Callbacks
•   Working with Validation        •   Callback Classes
    Errors
                                   •   Observers
•   Displaying Validation Errors
                                   •   Transaction Callbacks
    in the View

                                                       ROR Lab.
Validation Levels

• Native DB constraints - DB-dependent
• Client-side validations - javascript ?
• Controller-level validations - keep skinny
• Model-level validations - the best way

                                       ROR Lab.
“new” method
• A new object instantiated      vs. persisted?

• not yet to store to database : new_record?
• no validation called, but ...



                                             ROR Lab.
Validation Time Point
                             ActiveRecord
               Web Server

                Model              DB
                Validation
                   save
                  create
                  update
  clients
         Model-level validations


                                   ROR Lab.
Validation Event
        Triggering validations                   Skipping validations
        •   .create                              •   .decrement!
        •   .create!                             •   #decrement_counter
        •   .save                                •   .increment!
        •   .save!                               •   #increment_counter
        •   #update                              •   .toggle!
        •   .update_attributes                   •   .touch
        •   .update_attributes!                  •   #update_all
                                                 •   .update_attribute deprecated
                                                 •   .update_column
                                                 •   #update_counters



Ref.:   ActiveRecord::Persistence   ActiveRecord::CounterCache
                                                                      ROR Lab.
Call Validations
• valid? or invalid?




                       ROR Lab.
valid? vs Errors




By definition,
an object is valid if this collection is empty
after running validations.

                                                 ROR Lab.
Validation Helpers
• acceptance     • validates_associated
• confirmation    • validates_each
• exclusion      • validates_with
• format
• inclusion           :on
                            • save(default)
                            • create
• length                    • update

• numericality     :message
• presence
• uniqueness
                                  ROR Lab.
Common Options

:allow_nil - nil
:allow_blank - nil or whitespace
:message - overriding default error message
:on - :create / :update / :save



                                     ROR Lab.
Conditional Validation
  :if and :unless

    • A Symbol : a method name
    • A String : a really short condition
    • A Proc : an inline condition
  Grouping conditions : with_options

                                        ROR Lab.
Custom Validations
 • Custom validators modules
   : inherited from Two
      ★ ActiveModel::Validator
      ★ ActiveModel::EachValidator
      ★ Get the “record” argument as a
        parameter
 •   Custom validation methods
 •   Custom validation helpers

                                     ROR Lab.
Working with
  Validation Errors
• errors
• errors.messages
• errors.full_messages( or errors.to_a)
• errors[:attr] : for a specific attribute
• errors.add(:attr, message)(or errors[:attr]=)
• errors[:base] : object’s state as a whole
• errors.clear : intentionally to clear
• errors.size : count of errors
                                         ROR Lab.
Displaying Validation
 Errors in the View
★ gem ‘dynamic_form’   ★ Error   Messages CSS

                         .field_with_errors
                         #errorExplanation
                         #errorExplanation h2
                         #errorExplanation p
                         #errorExplanation ul li




                                      ROR Lab.
Validation Errors :                                                https://github.com/joelmoss/dynamic_form




                                                                 Error CSS
                                                                                               #error_explanation

                                                                                                                          #error_explanation h2
                                                                                                             #error_explanation p
<%= form_for(@product) do |f| %>

 <% if @product.errors.any? %>
  <div id="error_explanation">                                                                   #error_explanation ul li
   <h2><%= pluralize(@product.errors.count, "error") %>
      prohibited this product from being saved:
   </h2>
   <ul>
   <% @product.errors.full_messages.each do |msg| %>
    <li><%= msg %></li>
   <% end %>
   </ul>
  </div>
 <% end %>                                                                                                 .field_with_errors




                                                                                                          generated by scaffold

                                                          apps/assets/stylesheets/scaffolds.css.scss
                                                                                                                                ROR Lab.
Validation Errors :                          https://github.com/joelmoss/dynamic_form




                     Error CSS
                                                   #error_explanation
       <%= form_for(@product) do |f| %>
                                                                              #error_explanation h2
        <% if @product.errors.any? %>                            #error_explanation p
         <div id="error_explanation">
          <h2><%= pluralize(@product.errors.count, "error") %>
                                                  #error_explanation ul li
             prohibited this product from being saved:
          </h2>
          <ul>
          <% @product.errors.full_messages.each do |msg| %>
           <li><%= msg %></li>                            .field_with_errors
          <% end %>
          </ul>
         </div>
        <% end %>


                                                              generated by scaffold

              apps/assets/stylesheets/scaffolds.css.scss
                                                                                    ROR Lab.
Validation Errors :              https://github.com/joelmoss/dynamic_form




                                Error CSS
apps/assets
/stylesheets
/scaffolds.css.scss                        #error_explanation

.field_with_errors {                                                   #error_explanation h2
  padding: 2px;
  background-color: red;                                 #error_explanation p
  display: table;
}

#error_explanation {                        #error_explanation ul li
 width: 450px;
 border: 2px solid red;
 padding: 7px;
 padding-bottom: 0;
 margin-bottom: 20px;
 background-color: #f0f0f0;
 h2 {                                                  .field_with_errors
   text-align: left;
   font-weight: bold;
   padding: 5px 5px 5px 15px;
   font-size: 12px;
   margin: -7px;
   margin-bottom: 0px;
   background-color: #c00;
   color: #fff;
 }
 ul li {
                                                      generated by scaffold
   font-size: 12px;
   list-style: square;



                                                                           ROR Lab.
Validation Errors :


       ‘dynamic_form’
   • f.error_messages or
   • error_messages_for :product

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

    <%= f.error_messages %>

   <% end %>




                                      ROR Lab.
Validation Errors :                          https://github.com/joelmoss/dynamic_form




                 ‘dynamic_form’

                                                           generated by dynamic_form
        r_ tag
       e
:h ead                         :header_message
                                             :message




            <%= f.error_messages
              :header_message => "Invalid product!",
              :message => "You'll need to fix the following fields:",
              :header_tag => :h3 %>



                                                                                          ROR Lab.
Validation Errors :                           https://github.com/joelmoss/dynamic_form




                Error HTML
 config/initializers/custom_error_message_html.rb                              field object

 ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
   errors = Array(instance.error_message).join(',')
   unless html_tag =~ /^<label/
    %(#{html_tag}<span class="validation-error">&nbsp;#{errors}</span>).html_safe
   else
   %(#{html_tag}).html_safe
  end
 end




                               #{html_tag}            .validation-error




                                                                                     ROR Lab.
Validation Errors :                                                    https://github.com/joelmoss/dynamic_form




                    Error HTML
      <% if @product.errors.any? %>
       <div id="error_explanation">
        <h2><%= pluralize(@product.errors.count, "error") %> prohibited this product from being saved:</h2>
       </div>
      <% end %>




                                                                                                              ROR Lab.
감사합니다.
 
appendix
 
Active Record
 Counter Cache

• #decrement_counter
• #increment_counter   skipping
• #reset_counters      validations

• #update_counters
                               ROR Lab.
decrement vs
 decrement!




               ROR Lab.
toggle vs
 toggle!




            ROR Lab.
touch




        ROR Lab.
#update_all




              ROR Lab.
.update_column




             ROR Lab.
#update




          ROR Lab.
Validation Helpers :


               acceptance
     • a checkbox : a real or virtual attribute
     • “must be accepted”
     class Person  ActiveRecord::Base
       validates :terms_of_service, :acceptance = true



     class Person  ActiveRecord::Base
       validates :terms_of_service,
              :acceptance = { :accept = 'yes' }




                                                          ROR Lab.
Validation Helpers :


validates_associated
    • should if associated with other models
    • “is invalid”
     class Library  ActiveRecord::Base
       has_many :books
       validates_associated :books




    • should use on one end of your
       associations

                                          ROR Lab.
Validation Helpers :


           confirmation
    • a virtual attribute appended with
      “_confirmation”
    • “doesn’t match confirmation”
     class Person  ActiveRecord::Base
       validates :email, :confirmation = true
       validates :email_confirmation, :presence = true



     %= text_field :person, :email %
     %= text_field :person, :email_confirmation %



                                                         ROR Lab.
Validation Helpers :


                   exclusion
    • not included in a given set
      :any enumerable object
    • “is reserved”
     class Account  ActiveRecord::Base
       validates :subdomain,
         :exclusion = {
         :in = %w(www us ca jp),
         :message = Subdomain %{value} is reserved. }




                                                           ROR Lab.
Validation Helpers :


                       format
    • match a given regular expression
    • “is invalid”

     class Product  ActiveRecord::Base
       validates :legacy_code,
              :format = { :with = /A[a-zA-Z]+z/,
              :message = Only letters allowed }




                                                       ROR Lab.
Validation Helpers :


                   inclusion
    • included in a given set
      : any enumerable object
    • “is not included in the list”
     class Coffee  ActiveRecord::Base
       validates :size,
           :inclusion = {
             :in = %w(small medium large),
          :message = %{value} is not a valid size
         }




                                                       ROR Lab.
Validation Helpers :


                         length                        (1/2)

   • length constraint options
      - :minimum, :maximum, :in/:within, :is
      - a placeholder - %{count}

  class Person  ActiveRecord::Base
    validates :name, :length = { :minimum = 2 }
    validates :bio, :length = { :maximum = 500 }
    validates :password, :length = { :in = 6..20 }
    validates :registration_number, :length = { :is = 6 }




                                                               ROR Lab.
Validation Helpers :


                        length                     (2/2)

   • options
     - :wrong_length, :too_long, :too_short
     - a placeholder - %{count}
   class Essay  ActiveRecord::Base
     validates :content, :size = {
       :minimum   = 300,
       :maximum   = 400,
       :tokenizer = lambda { |str| str.scan(/w+/) },
       :too_short = must have at least %{count} words,
       :too_long  = must have at most %{count} words
     }




                                                            ROR Lab.
Validation Helpers :


            numericality                                 (1/2)

    • only numeric values
      : (+/−) integer/floating point
    • “is not a number”
     class Person  ActiveRecord::Base
       validates :email, :confirmation = true
       validates :email_confirmation, :presence = true



     %= text_field :person, :email %
     %= text_field :person, :email_confirmation %



                                                         ROR Lab.
Validation Helpers :


               numericality                                                          (2/2)


     class Player  ActiveRecord::Base
       validates :points, :numericality = true
       validates :games_played,
            :numericality = { :only_integer = true }

                                                                   /A[+−]?d+Z/



   •:greater_than               ➡   “must   be   greater than %{count}”
   •:greater_than_or_equal_to   ➡   “must   be   greater than or equal to %{count}
   •:equal_to                   ➡   “must   be   equal to %{count}”
   •:less_than                  ➡   “must   be   less than %{count}”
   •:less_than_or_equal_to      ➡   “must   be   less than or equal to %{count}
   •:odd                        ➡   “must   be   odd”
   •:even                       ➡   “must   be   even”




                                                                                     ROR Lab.
Validation Helpers :


                   presence (1/2)
   • empty or whitespaces
   • “can’t be empty”
    class Person  ActiveRecord::Base
      validates :name, :login, :email, :presence = true
    end




    class LineItem  ActiveRecord::Base
      belongs_to :order
      validates :order_id, :presence = true



                                                           ROR Lab.
Validation Helpers :


                     presence (2/2)
   • empty or whitespaces
   • “can’t be empty”

                                               ✘
    class Person  ActiveRecord::Base
      validates :is_admin, :presence = true
    end              a boolean field ➞ false.blank? is true


    class Person  ActiveRecord::Base
      validates :is_admin,
             :inclusion = { :in = [true, false] }



                                                             ROR Lab.
Validation Helpers :


               uniqueness
   • add_index :table_name, :column_name, unique = true
   • “has already been taken”
    class Account  ActiveRecord::Base
      validates :email, :uniqueness = true                  other
    end                                                    attributes
    class Holiday  ActiveRecord::Base
      validates :name, :uniqueness = { :scope = :year,
        :message = should happen once per year }
    end
    class Person  ActiveRecord::Base
      validates :name,
           :uniqueness = { :case_sensitive = false }


                                                                ROR Lab.
Validation Helpers :


         validates_with (1/2)
   • a separate class for validation
   • no default validate error message
    class Person  ActiveRecord::Base
      validates_with GoodnessValidator,[:if/:unless/:on]
    end
     
    class GoodnessValidator  ActiveModel::Validator
      def validate(record)
        if record.first_name == Evil
          record.errors[:base]  This person is evil
        end
      end



                                                           ROR Lab.
Validation Helpers :


            validates_with (2/2)
   • any additional options ➞ options
  class Person  ActiveRecord::Base
    validates_with GoodnessValidator, :fields =
                            [:first_name, :last_name]
  end
   
  class GoodnessValidator  ActiveModel::Validator
    def validate(record)
      if options[:fields].any?{|field| record.send(field) == Evil }
        record.errors[:base]  This person is evil
      end
    end
  end




                                                                     ROR Lab.
Validation Helpers :


                   validates_each
            • no predefined validation function
               ➞a block
            • no default error message
class Person  ActiveRecord::Base
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /A[a-z]/
  end
end




                                                                                  ROR Lab.
Common Validation Options :


                  :allow_nil

    class Coffee  ActiveRecord::Base
      validates :size,
        :inclusion = { :in = %w(small medium large),
        :message = %{value} is not a valid size },
        :allow_nil = true




                                                         ROR Lab.
Common Validation Options :


            :allow_blank
    • nil or an empty string
    class Topic  ActiveRecord::Base
      validates :title,
             :length = { :is = 5 },
             :allow_blank = true
    end
     
    Topic.create(title = ).valid?  # = true




                                                    ROR Lab.
Common Validation Options :


                                      :on
          • when the validation should happen
class Person  ActiveRecord::Base
  # it will be possible to update email with a duplicated value
  validates :email, :uniqueness = true, :on = :create
 
  # it will be possible to create the record with a non-numerical age
  validates :age, :numericality = true, :on = :update
 
  # the default (validates on both create and update)
  validates :name, :presence = true, :on = :save




                                                                        ROR Lab.
Conditional Validation


             :if  :unless                         (1/4)

    • Using a Symbol : a method name
    class Order  ActiveRecord::Base
      validates :card_number, :presence = true,
             :if = :paid_with_card?
     
      def paid_with_card?
        payment_type == card
      end




                                                    ROR Lab.
Conditional Validation


             :if  :unless                     (2/4)

    • Using a String : a really short condition
    class Person  ActiveRecord::Base
      validates :surname, :presence = true,
             :if = name.nil?




                                                ROR Lab.
Conditional Validation


              :if  :unless                             (3/4)

    • Using a Proc : an inline condition
    class Account  ActiveRecord::Base
      validates :password, :confirmation = true,
        :unless = Proc.new { |a| a.password.blank? }
                                    a model
                                     object




                                                         ROR Lab.
Conditional Validation


              :if  :unless                                (4/4)

    • grouping conditional validations
                                             condition
                                              object
   class User  ActiveRecord::Base
     with_options :if = :is_admin? do |admin|
       admin.validates :password, :length = { :minimum = 10 }
       admin.validates :email, :presence = true
     end




                                                              ROR Lab.
Custom Validations :


   Custom Validators (1/2)
   • to extend ActiveMode::Validator
   • to validate the state of whole record
   class MyValidator  ActiveModel::Validator
     def validate(record)
       unless record.name.starts_with? 'X'
         record.errors[:name]  'Need a name starting with X please!'
       end
     end
   end
    
   class Person
     include ActiveModel::Validations
     validates_with MyValidator
   end




                                                                         ROR Lab.
Custom Validations :


   Custom Validators (2/2)
   • to extend ActiveMode::EachValidator
   • to validate individual attributes
  class EmailValidator  ActiveModel::EachValidator
    def validate_each(record, attribute, value)
      unless value =~ /A([^@s]+)@((?:[-a-z0-9]+.)+[a-z]{2,})z/i
        record.errors[attribute]  (options[:message] || is not an email)
      end
    end
  end
                                                     EmailValidator
  class Person  ActiveRecord::Base
    validates :email, :presence = true, :email = true
  end




                                                                               ROR Lab.
Custom Validations :


     Custom Methods (1/2)
   • to verify the state of models
  class Invoice  ActiveRecord::Base
    validate :expiration_date_cannot_be_in_the_past,
      :discount_cannot_be_greater_than_total_value
   
    def expiration_date_cannot_be_in_the_past
      if !expiration_date.blank? and expiration_date  Date.today
        errors.add(:expiration_date, can't be in the past)
      end
    end                                                         EmailValidator
   
    def discount_cannot_be_greater_than_total_value
      if discount  total_value
        errors.add(:discount, can't be greater than total value)
      end
    end
  end



                                                                                 ROR Lab.
Custom Validations :


     Custom Methods (2/2)
   • to verify the state of models
  class Invoice  ActiveRecord::Base
    validate :active_customer, :on = :create
   
    def active_customer
      errors.add(:customer_id, is not active)
                        unless customer.active?
    end




                                                  ROR Lab.
Custom Validations :


       Custom Helpers
   • to reuse in several different models
   • to put in config/initializers
  ActiveRecord::Base.class_eval do
    def self.validates_as_choice(attr_name, n, options={})
      validates attr_name,
        :inclusion = { { :in = 1..n }.merge!(options) }
    end
  end



   class Movie  ActiveRecord::Base
     validates_as_choice :rating, 5
   end


                                                             ROR Lab.
Validation Errors :


                                errors
   • an instance of ActiveModel::Errors
   • key : attribute name
       value : an array of strings with all errors
   class Person  ActiveRecord::Base
     validates :name, :presence = true, :length = { :minimum = 3 }
   end
    
   person = Person.new
   person.valid? # = false
   person.errors
    # = {:name =
          [can't be blank, is too short (minimum is 3 characters)]}
    
   person = Person.new(:name = John Doe)
   person.valid? # = true
   person.errors # = []



                                                                          ROR Lab.
Validation Errors :


                            errors[ ]
   • to check the error message of a specific
       attribute
   class Person  ActiveRecord::Base
     validates :name, :presence = true, :length = { :minimum = 3 }
   end
    
   person = Person.new(:name = John Doe)
   person.valid? # = true
   person.errors[:name] # = []
    
   person = Person.new(:name = JD)
   person.valid? # = false
   person.errors[:name] # = [is too short (minimum is 3 characters)]
    
   person = Person.new
   person.valid? # = false
   person.errors[:name]
    # = [can't be blank, is too short (minimum is 3 characters)]




                                                                          ROR Lab.
Validation Errors :


                     errors.add                                         (1/2)

   • to manually add messages of a specific
      attribute
   class Person  ActiveRecord::Base
     def a_method_used_for_validation_purposes
       errors.add(:name, cannot contain the characters !@#%*()_-+=)
     end
   end
    
   person = Person.create(:name = !@#)
    
   person.errors[:name]
    # = [cannot contain the characters !@#%*()_-+=]
    
   person.errors.full_messages
    # = [Name cannot contain the characters !@#%*()_-+=]




                                                                          ROR Lab.
Validation Errors :


                    errors.add                                        (2/2)

                                  -or-
   class Person  ActiveRecord::Base
     def a_method_used_for_validation_purposes
       errors[:name] = cannot contain the characters !@#%*()_-+=)
     end
   end
    
   person = Person.create(:name = !@#)
    
   person.errors[:name]
    # = [cannot contain the characters !@#%*()_-+=]
    
   person.errors.to_a
    # = [Name cannot contain the characters !@#%*()_-+=]




                                                                        ROR Lab.
Validation Errors :


              errors[:base]
   • related to the object’s state as a whole
   • an array
   class Person  ActiveRecord::Base
     def a_method_used_for_validation_purposes
       errors[:base]  This person is invalid because ...
     end
   end




                                                               ROR Lab.