How to Rippling validation effects from child to parent?


#1

Hi to everyone,

I’m new to rails and am experiencing problems with validations.
I have the following models:

class Client < ActiveRecord::Base
validates_presence_of :cognome, :nome, :data_di_nascita,:badge
has_one :badge

end

class Badge < ActiveRecord::Base
belongs_to :client
validates_presence_of :numero
validates_numericality_of :numero, :only_integer => true
validates_uniqueness_of :numero
validates_presence_of :tipo

private

def validate
errors.add(:client_id,“Badge già assegnato”) if (self.client_id !=
nil) and (Badge.find(:all, :conditions => “client_id =
#{self.client_id}”).size > 1)
end

end

Whenever I try assigning the same client_id to two different Badge’s
instances, and then issue a “save!”, I get an exception as expected.

If I try the following:

c,d = Client.find(:all)
b1,b2 = Badge.find(:all)
c.badge = b1
d.badge = b1
c.save!
d.save!

No exception gets raised even though b1.errors.size == 1!
Shouldn’t this error ripple back to the Client instances and raise an
exception upon calling “save!”?

I’ve found in the Rails API that there’s a “validates_associates” method
that
extends validations checking to model’s associates.
Is this one the way to go?

Thanks in advance for your help.
Regards,
Carmine


#2

On Feb 7, 9:31 am, Carmine M. removed_email_address@domain.invalid
wrote:

class Client < ActiveRecord::Base
validates_presence_of :cognome, :nome, :data_di_nascita,:badge
has_one :badge

end

When you validate the presence of an association refer directly to the
column name:

validates_presence_of :cognome, :nome, :data_di_nascita,:badge_id

def validate
errors.add(:client_id,“Badge già assegnato”) if (self.client_id !=
nil) and (Badge.find(:all, :conditions => “client_id =
#{self.client_id}”).size > 1)
end

It looks like your validate method is trying to ensure the uniqueness
of client_id if it’s not nil. You can do that all with
validates_uniqueness_of:

validates_uniqueness_of :client_id, :allow_nil => true

Aaron


#3

Aaron, first off thanks for your reply.

Aaron wrote:

On Feb 7, 9:31 am, Carmine M. removed_email_address@domain.invalid
wrote:

class Client < ActiveRecord::Base
validates_presence_of :cognome, :nome, :data_di_nascita,:badge
has_one :badge

end

When you validate the presence of an association refer directly to the
column name:

validates_presence_of :cognome, :nome, :data_di_nascita,:badge_id

If I use the validates you proposed above, then upon validating Rails
complains
about “Client” not having any “badge_id”.
That means that writing something like:

c,d = Client.find(:all)
b1,b2 = Badge.find(:all)

where both “c” and “d” have “badge” set to nil will produce:

c.valid? -> false
d.valid? -> false

which is ok.

While doing:

c.badge = b1
d.badge = b2
c.valid? -> false

which is not ok. :slight_smile:

This doesn’t happen if I exclude from “validates_presence_of” the
“:badge_id” field.

def validate
errors.add(:client_id,“Badge gi� assegnato”) if (self.client_id !=
nil) and (Badge.find(:all, :conditions => “client_id =
#{self.client_id}”).size > 1)
end

It looks like your validate method is trying to ensure the uniqueness
of client_id if it’s not nil. You can do that all with
validates_uniqueness_of:

validates_uniqueness_of :client_id, :allow_nil => true

Thanks, this was nicer :slight_smile:

Sadly I’ve not solved my problem yet.

What I would like to achieve is:

  • Each “Client” has to have one and only one Badge
  • Each “Badge” can be associated at one Client only, or it can be left
    unassociated

It seemed to me that the way to go was to:

  • Declare that Client has_one :badge
  • Declare that Badge belongs_to :client
  • Validate uniqueness of :client_id (allowing for nil) on the Badge
    model

Thus far I can’t work it out.


#4

Aaron wrote:

Sorry about the mistake on the has_one :badge association. I forgot
that the foreign key reference is on the belongs_to side.

Don’t worry Aaron :slight_smile: I can only thank you for trying helping me.

I took another look at your models and what you want to accomplish and
came up with this:

class Client < ActiveRecord::Base
has_one :badge

ensure client has a badge

def validate
errors.add_to_base(‘Client must have a badge!’) if badge.nil?
end
end

class Badge < ActiveRecord::Base
belongs_to :client

ensure client has only one badge

validates_uniqueness_of :client_id, :allow_nil => true
end

When validating that a Client has a Badge there is no attribute to
associate the error with so use “add_to_base” to invalidate the entire
object if the badge association is not present.

This sounds nice.
Still I’m back to square one.

I forgot to say that no two or more different clients can have the same
badge associated.
This is something I think has to be worked out defining a validate in
the Client model, this way:

def validate
errors.add_to_base(“client must have a badge!”) if badge.nil?
errors.add_to_base(“badge already taken!”) if Badge.count(:conditions
=> “client_id = #{self.id}”) > 0
end

Doing this way, the tests produce no errors and the expected results.
So, is this the only way to go, or are there any (perhaps better) one
roads to choose?

Thanks again for your precious help.
Regards,
Carmine


#5

I forgot to say that no two or more different clients can have the same
badge associated.
This is something I think has to be worked out defining a validate in
the Client model, this way:

def validate
errors.add_to_base(“client must have a badge!”) if badge.nil?
errors.add_to_base(“badge already taken!”) if Badge.count(:conditions
=> “client_id = #{self.id}”) > 0
end

Doing this way, the tests produce no errors and the expected results.
So, is this the only way to go, or are there any (perhaps better) one
roads to choose?

Just spoke too soon :frowning:

As soon as “validate” gets called, a “save” has already been issued on
the associated badge’s instance.

Guess it’s time to sleep :frowning:


#6

Sorry about the mistake on the has_one :badge association. I forgot
that the foreign key reference is on the belongs_to side.

I took another look at your models and what you want to accomplish and
came up with this:

class Client < ActiveRecord::Base
has_one :badge

ensure client has a badge

def validate
errors.add_to_base(‘Client must have a badge!’) if badge.nil?
end
end

class Badge < ActiveRecord::Base
belongs_to :client

ensure client has only one badge

validates_uniqueness_of :client_id, :allow_nil => true
end

When validating that a Client has a Badge there is no attribute to
associate the error with so use “add_to_base” to invalidate the entire
object if the badge association is not present.

Aaron


#7

Your assumptions and understanding of what I wanted to do, are right.

Aaron wrote:

Now you may want to prevent a client from taking a badge that is
already in use by another client. That’s what it looks like your
trying to do with this line:
errors.add_to_base(“badge already taken!”) if Badge.count(:conditions
=> “client_id = #{self.id}”) > 0

Desperate attempt! :slight_smile:

However, this line is checking that current client does not already
have another badge assigned. Which is already addressed by the
validates_uniqueness_of statement I mentioned above.

This is not exactly pretty, but it should work:
def validate
if badge.nil?
# don’t let the badge association be nil
errors.add_to_base(“client must have a badge!”)
else
# pull the old version of badge from the database
old_badge = Badge.find(badge.id)
# if the old badge already had a client_id set and its not for
this client then add an error
errors.add_to_base(“badge already taken!”) if old_badge.client_id
and old_badge.client_id != id
end
end

Nope, this doesn’t work. At least it doesn’t work the way I would like
to.
I mean, let’s say there are 2 clients: “c” and “d”, and 2 badges “b1”
and “b2”.
At first, we have c.badge == b1, and d.badge == nil.
c.valid? -> true
d.valid? ->false

When I issue:
d.badge = b1 (which is already been taken by “c”)

I obtain:
c.valid? -> false
d.valid? -> true

Now it seems that every and each conditions posed is respected but,
rails messed
up with other consolidate row in the database.

When I did: d.badge = b1, I was expecting rails to leave “d” in a not
valid state, not to change c
and make d valid! :slight_smile:

Now, I’m a total newbie of Rails (sill reading the book from pragmatic
programmers), but this
thing is driving me insane :slight_smile:

Your turn :wink:


#8

I forgot to say that no two or more different clients can have the same
badge associated.
This is something I think has to be worked out defining a validate in
the Client model, this way:

If I understand your database schema correctly its impossible for two
clients to have the same badge. Your badges table has a “client_id”
column, right? So a particular badge can only be associated with 1
client at a time, there’s no need to validate something that can never
happen.

Your other concern is preventing a client from having more than 1
badge. That’s what this line in the Badge model takes care of:
validates_uniqueness_of :client_id, :allow_nil => true

Now you may want to prevent a client from taking a badge that is
already in use by another client. That’s what it looks like your
trying to do with this line:
errors.add_to_base(“badge already taken!”) if Badge.count(:conditions
=> “client_id = #{self.id}”) > 0

However, this line is checking that current client does not already
have another badge assigned. Which is already addressed by the
validates_uniqueness_of statement I mentioned above.

This is not exactly pretty, but it should work:
def validate
if badge.nil?
# don’t let the badge association be nil
errors.add_to_base(“client must have a badge!”)
else
# pull the old version of badge from the database
old_badge = Badge.find(badge.id)
# if the old badge already had a client_id set and its not for
this client then add an error
errors.add_to_base(“badge already taken!”) if old_badge.client_id
and old_badge.client_id != id
end
end

Aaron


#9

Well, it seems that overriding default accessors for badge in the Client
model
helps solving things.

I mean, rewriting Client model like this:
class Client < ActiveRecord::Base
has_one :badge
validates_presence_of :nome

def badge
@badge
end

def badge=(value)
if Badge.find(:all, :conditions => “client_id <> #{self.id} and id =
#{value.id}”).size > 0 then
errors.add_to_base(“Badge is already taken!”)
@badge = nil
else
@badge = value
end
end

private
def validate
errors.add_to_base(“Badge can’t be blank!”) if self.badge.nil?
end
end

Almost works like I would like to. There’s only one thing to sort out.
When assigning the same badge to two different clients, now, I would
expect
to have two error messages added to the “base”. Well, this seems not to
happen.
A little step forward :slight_smile:


#10

Let’s put the “End” word to this.

My assumptions were completely wrong.

I thought that “has_one” worked quite the opposite way it really does.

Swapping “has_many” and “belongs_to” in the models mentioned in this
thread, reduced the amount
of code and, what’s more important, it made everything work as expected.

That’s what means to be a newbie :wink: