Forum: Ruby on Rails How to Rippling validation effects from child to parent?

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
74ba9c207c0b60aa996fbc7db1c28cf9?d=identicon&s=25 Carmine Moleti (carminem)
on 2007-02-07 17:31
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
8da92d4ed91aa12535f6d870fa76b25e?d=identicon&s=25 Aaron (Guest)
on 2007-02-07 18:11
(Received via mailing list)
On Feb 7, 9:31 am, Carmine Moleti <rails-mailing-l...@andreas-s.net>
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
74ba9c207c0b60aa996fbc7db1c28cf9?d=identicon&s=25 Carmine Moleti (carminem)
on 2007-02-07 20:45
Aaron, first off thanks for your reply.

Aaron wrote:
> On Feb 7, 9:31 am, Carmine Moleti <rails-mailing-l...@andreas-s.net>
> 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. :)

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 :)

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.
8da92d4ed91aa12535f6d870fa76b25e?d=identicon&s=25 Aaron (Guest)
on 2007-02-07 21:47
(Received via mailing list)
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
74ba9c207c0b60aa996fbc7db1c28cf9?d=identicon&s=25 Carmine Moleti (carminem)
on 2007-02-08 00:00
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 :) 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
74ba9c207c0b60aa996fbc7db1c28cf9?d=identicon&s=25 Carmine Moleti (carminem)
on 2007-02-08 00:19
> 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 :(

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

Guess it's time to sleep :(
8da92d4ed91aa12535f6d870fa76b25e?d=identicon&s=25 Aaron (Guest)
on 2007-02-08 00:53
(Received via mailing list)
> 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
74ba9c207c0b60aa996fbc7db1c28cf9?d=identicon&s=25 Carmine Moleti (carminem)
on 2007-02-08 07:48
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! :)

> 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! :)

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

Your turn ;)
74ba9c207c0b60aa996fbc7db1c28cf9?d=identicon&s=25 Carmine Moleti (carminem)
on 2007-02-08 10:19
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 :)
74ba9c207c0b60aa996fbc7db1c28cf9?d=identicon&s=25 Carmine Moleti (carminem)
on 2007-02-08 22:27
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 ;)
This topic is locked and can not be replied to.