Rails 3 can't perform validation for persited object when use collection_singular_ids=ids method
Asked Answered
S

2

5

Is there any way to avoid automatically saving object while assigning collection attributes(collection_singular_ids=ids method)?

for example, I have the following Test and Package model, Package has many tests. User can build package bundle with number of tests.

# test.rb
class Test < ActiveRecord::Base
end

# package.rb
class Package < ActiveRecord::Base
  has_many :package_tests 
  has_many :tests, :through => :package_tests
  belongs_to :category

  validate :at_most_3_tests

  private
  # tests count will differ depends on category.
  def at_most_3_tests
    errors.add(:base, 'This package should have at most three tests') if  test_ids.count > 3
  end
end

# package_test.rb
class PackageTest < ActiveRecord::Base
  belongs_to :package
  belongs_to :test

  validates_associated :package
end

No issue on validation when package object is new.

1.9.2 :001> package = Package.new(:name => "sample", :cost => 3.3, :test_ids => [1,2,3,4])
=> #<Package id: nil, name: "sample", cost: 3.3> 
1.9.2 :002> package.test_ids
=> [1, 2, 3, 4] 
1.9.2 :003> package.save
=> false 
1.9.2 :004> package.save!
ActiveRecord::RecordInvalid: Validation failed: This package should have at most three tests
1.9.2: 005> package.test_ids = [1,2]
=> [1, 2] 
1.9.2 :005> package.save!
=> true

But I couldn't hit at_most_3_tests method with persisted package object.

Join table record is created immediately when assigning test ids

1.9.2: 006> package
=> #<Package id: 1, name: "sample", cost: 3.3> 
1.9.2: 007> package.test_ids
=> [1,2]
1.9.2: 007> package.test_ids = [1,2,3,4,5]
=> [1,2,3,4,5]
1.9.2: 008> package.test_ids 
=> [1,2,3,4,5]

Client requirement is drop-down interface for selection of multiple tests in package form and also used select2 jquery plugin for drop-down. Rhmtl code looks like

<%= form_for @package do |f| %>
  <%= f.text_field :name %>
  <div> <label>Select Tests</label> </div>
  <div>
    <%= f.select "test_ids", options_for_select(@tests, f.object.test_ids), {}, { "data-validate" => true, :multiple => true} %>
  </div>

Please help me to fix this issue.

Strohl answered 28/10, 2014 at 15:18 Comment(0)
O
5

For limit number of associations

You can use the following validations as the following instead of your method:

has_many :tests, :length => { :maximum => 3 }

For using Multiple select

I have this issue before, and I solved it using the following code:

<%= f.select(:test_ids, options_from_collection_for_select(@tests, :id, :name,  @promotion.test_ids), {}, {multiple: true, "data-validate" => true}) =>

I think options_from_collection_for_select, read categories of post example from this link may help you.

For Validation

I used validates_associated, as the following:

 validates_associated :tests

For get the old attributes for persisted object

You can use reload for active record as the following:

1.9.2: 006> package
=> #<Package id: 1, name: "sample", cost: 3.3> 
1.9.2: 007> package.test_ids
=> [1,2]
1.9.2: 007> package.test_ids = [1,2,3,4,5]
=> [1,2,3,4,5]
1.9.2: 007> package.reload
=> #<Package id: 1, name: "sample", cost: 3.3> 
1.9.2: 008> package.test_ids 
=> [1,2]

Or you can check validation of package object, if it is false reload it:

unless package.valid?
  package.reload
end
Outcrop answered 22/12, 2014 at 9:17 Comment(5)
I can't able to call package.valid? because whenever assigning array of ids then automatically save action is triggered. For example package.test_ids = [1,2,3,4,5]Strohl
@ManivannanJeganathan I update my answer to use this validation has_many :tests, :length => { :maximum => 3 }Outcrop
My validation is not only based on length. I just given example code. params[:package] should always have test_ids parameter like {:cost => 3.4, :test_ids => [1,2,3,4,5]}. Simply I need to hit validation method in package model and render form if fails validation. I can hit validation method with help of your answer by calling validates_associated :package in PackageTest model. But if validation fails related to test_ids validation, exception raised instead of returning true or false. Ex code if @package.update_attributes params[:package] else render "form"Strohl
I was trying validates_associated :package in PackageTest instead of validates_associated :tests in Package model. it solved my problem. Thanks for helpStrohl
When I calling test_ids in at_most_3_tests method always returns database results instead parameter values. How to retrieve values from memory?Strohl
A
3

If you're manually assigning the test_ids in the controller, I'd suggest updating the entire object with nested attributes instead. This assumes that params[:package][:test_ids] is set to your list of test ids (which Mohamed's answer will help with). So your controller action would look something like this:

def update
  package = Package.find(params[:id])
  package.update_attributes params[:package]
end

This will update everything at once in an ActiveRecord/database transaction. This means that if the validation fails, all of the changes will be rolled back, so it won't matter that the tests got saved. More information is here.

Also, I'd recommend calling tests.size instead of test_ids.count, since the replacement will tend to generate a better query (or not have to go to the database at all).

Afterlife answered 28/12, 2014 at 3:35 Comment(6)
Fine.But there is problem when calling update_attributes with test_ids parameter, it will raise exception instead of returning true or false if validation fails related to test_ids validation(at_most_3_tests method). Any suggestion? Example code: if package.update_attributes params[:package]Strohl
I used validates_associated :package in PackageTest modelStrohl
validates_associated shouldn't raise an exception by default. What error are you getting?Afterlife
Validation failed: Package is invalid because of parameter test_ids(#=> [1,2,3,4,5]) which is validated in Package model.Strohl
What if you use validates_associated :tests in your Package model instead? I'm not sure that having it in PackageTest would produce the desired result anyway. Also, are you sure you're not calling update_attributes! or save! anywhere? That would result in an exception for a validation error, which is not what you want in this case.Afterlife
Thanks. Working as expected If I use validates_associated :tests in Package model. But If I try validates_associated :package in PackageTest model will raise exception instead of returning true or false. I am sure that I am not calling save! or update_attributes! anywhere. I have attached demo project for your reference.Strohl

© 2022 - 2024 — McMap. All rights reserved.