Lefty
Lefty

Reputation: 695

Ruby on Rails: Validations on Form Object are not working

Validations on Form Object does not work, What is wrong with my code?

Please read the two cases posted. The first case has validation working, the second case does not.

Case 1

#Profile Model:

class Profile < ApplicationRecord
  belongs_to :profileable, polymorphic: true

  validates_presence_of :age
  validates :age, numericality: { greater_than_or_equal_to: 0, 
                                   only_integer: true, 
                                   :allow_blank => true
                                }
end

Validation Test from Console:

 p= Profile.new =>  #<Profile id: nil, age: nil>
 p.age = "string" => "string"
 p.save => False
 p.errors.full_messages
 => ["Profileable must exist", "Age is not a number"] 

 Profile.create(age:"string").errors.full_messages
 => ["Profileable must exist", "Age is not a number"] 

Validation directly on the model works

Case 2

#Form Object Registration:Profile:

module Registration
  class Profile
    include ActiveModel::Model
    
    validates_presence_of :age
    validates :age, numericality: { greater_than_or_equal_to: 0, 
                                    only_integer: true, 
                                    :allow_blank => true
                                   }
    attr_reader :user
    
    delegate :age , :age=, to: :profile
    
    
    def persisted?
      false
    end
    
    def user
      @user ||= User.new
    end
    
    def teacher
       @teacher ||= user.build_teacher
     end
      
     def profile
       @profile ||= teacher.build_profile
     end
  
     def save
       if valid?
         profile.save!
           true
       else
         false
       end
     end

    def submit(params)
      profile.attributes = params.slice(:age)
      if valid?
        profile.save!
      end
      self
    end 
    def self.model_name
      ActiveModel::Name.new(self, nil, "User")
    end
    
    def initialize(user=nil, attributes={})
      @user = user
    end
 end
end

#Profile Model:

class Profile < ApplicationRecord
  belongs_to :profileable, polymorphic: true
end
Validation Test from Console on form object does not work
 a=Registration::Profile.new(User.first)
 a.age = "string"
 a.save => true
 a.errors.full_messages
 => [] 

Upvotes: 0

Views: 504

Answers (1)

Joshua Cheek
Joshua Cheek

Reputation: 31726

It is returning 0 because it delegates the age accessor to the profile model. When you set it, it passes that through to the underlying profile, which keeps track of the value that you set (in profile.attributes_before_type_cast), but when you call the age getter on it (which the delegator does), it returns the typecast value instead (in profile.attributes)

p = Profile.new age: "omg"
p.attributes_before_type_cast # => {"id"=>nil, "user_id"=>nil, "age"=>"omg"}
p.attributes                  # => {"id"=>nil, "user_id"=>nil, "age"=>0}
p.age                         # => 0

I modified your example to store the attributes on the Registration::Profile instance, and only copy them over after passing the active model's validations. There's multiple ways to do this, but I used ActiveModel::Attributes to do that, so that it would behave like ActiveRecord, since that's probably most familiar and compatible.

Now, instead of delegating the age to the profile, you declare it with attribute :age. You don't have to use these if you don't want, but you don't want to store it on the underlying profile object (eg you could use an attr_accessor if you wanted, but then you'd also have to manually build the hash you pass before saving the underlying profile).

Here's my version:

# Setup a database to test it with
require 'active_record'
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
ActiveRecord::Schema.define do
  self.verbose = false
  create_table :users do |t|
    t.string :name
  end
  create_table :profiles do |t|
    t.integer :user_id
    t.integer :age
  end
end

# The underlying models
User    = Class.new(ActiveRecord::Base) { has_one :profile }
Profile = Class.new(ActiveRecord::Base) { belongs_to :user }

# The wrapping model
module Registration
  class Profile
    attr_reader :user, :profile

    include ActiveModel::Attributes
    attribute :age

    include ActiveModel::Model
    validates_presence_of :age
    validates :age, numericality: { greater_than_or_equal_to: 0,  only_integer: true,  allow_blank: true }
    
    def initialize(user)
      @user    = user
      @profile = user.profile || user.build_profile

      # to set @attributes
      super()

      # start with the profile's current attributes
      self.attributes = profile.attributes.slice(*@attributes.keys)
    end
  
    def save
      return false unless valid?
      profile.attributes = attributes # copy our attributes to the underlying model
      profile.save!                   # we expect it to save, so explode if not
    end
  end
end

u = User.create!
p = Registration::Profile.new(u)

# Invalid example
p.age = "string"
p.save                 # => false
p.errors.full_messages # => ["Age is not a number"]
p.age                  # => "string"
p.profile.age          # => nil
p.profile.persisted?   # => false

# Valid example
p.age = "123"
p.save                 # => true
p.errors.full_messages # => []
p.age                  # => "123"
p.profile.age          # => 123
p.profile.persisted?   # => true

# Initialize with an existing profile
Registration::Profile.new(u).age # => 123

Upvotes: 2

Related Questions