views:

505

answers:

1

I have a user model with a HABTM relationship to groups. I do not want a user to be able to be in more than 5 groups, so would like to validate the length of the HABTM relationship.

On the edit user page I have a list of checkboxes where the user can select the groups they want to be in (I'm using formtastic for the form).

In my users controller I'm calling:

@user.update_attributes(params[:user])

which is causing rails to update the associations automatically.

In my user model I have the following:

def validate
    if self.groups.length > 5
        self.errors.add(:groups, "cannot have more than 5 groups")
    end
end

This is causing the form to fail the validation, but the update_attributes call has already updated the database to reflect the changes to the associated groups. This way every time a user clicks the save button their group associations are saved, even though the record is invalid.

What's the best way to solve this?

I think perhaps the validation needs to be on the group model instead of the user model, would this work? Ideally I'd like to update the associated groups without saving the record, do the validation, and THEN save the record.

+5  A: 

You had two problems here:

  1. You're overriding validations
  2. The order of operations in saving is causing problems.

You're overwriting the validate method which is a bad thing, because the built in behaviour to deny records with validation errors to be saved to the database. To add custom validations you want to do this:

validate :maximum_group_length

def maximum_group_length
    if self.groups.length > 5
        self.errors.add(:groups, "cannot have more than 5 groups")
    end
end

However, the nature of HABTM relationships requires you to do it as an after_save callback. Just because of the order that things are done. user.groups is based on the implicit join table and there for isn't updated until the join table is updated.

If you're trying to validate as part of a callback (before_save, after_creation, etc.), then adding an error to the object won't trigger a roll back. Callbacks will only trigger a rollback if they return false. This will handle the after save implementation the question suggest.

after_save :validate_maximum_group_length

def validate_maximum_group_length
    if self.groups.length > 5
        self.errors.add(:groups, "cannot have more than 5 groups")
        return false
    end
end

Another solution is to use an explicit join model. And a has_many :through relationship. The join model's table is updated in the update statement. Where as the has_many :through and HABTM relationships update the relationship after the save.

class User < ActiveRecord::Base
  has_many :user_groups
  has_many :groups, :through => user_groups, allow_destroy

  validate :max_group_length
    errors.add(:groups, "cannot have more than 5 groups") if self.user_groups.length > 5
  end

end

class UserGroup < ActiveRecord::Base
  belongs_to :user
  belongs_to :group
end

class Group < ActiveRecord::Base
  has_and_belongs_to_many :users
end

HABTM implicitly uses a join table, so it doesn't need to be changed on the group side.

However you will need to modify your form to update the form to supply group_id in the params hash as params[:user][:user_group_attributes][0][:group_id][3]

EmFi
This doesn't solve it, it's still persisting the changes.
jonnii
Looks like I misdiagnosed your problem, I thought you were validating as a callback. The problem was that you were redefining validate.
EmFi
I've moved the validation into a custom validation routine as suggested, but this still doesn't solve the problem.
jonnii
Added a list of other possible causes to my solution. The previously posted code works.
EmFi
Oops was testing with just a has_many relationship. Corrected and provided explanation of the HABTM problem.
EmFi
I'm going to give this a thorough going through tomorrow. Thanks for taking the time to answer.
jonnii
I've gone the route of using an after_save callback, but the changes to the model are still being persisted...
jonnii
I've just tried the HM:T association and it's still not validating...! I must be doing something hilarious wrong, but missing it.
jonnii
I'm sorry, I've got no more suggestions. You could try debugging it your self by inspecting things in various states of callbacks (validate, after_save, etc)
EmFi
I worked out what the problem was, it seems in the controller I was calling `@profile.attributes = params[:profile]` instead of update_attributes. The model was working fine, I was just being stupid, however I updated it as per your comments, so this was definitely a worthwhile exercise.
jonnii
nice answer. very detailed.
Aeon