Reputation: 695
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.
#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
#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
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