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.

2 Answers2

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
Mohamed Yakout
  • 2,868
  • 1
  • 25
  • 45
  • 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] – Manivannan Jeganathan Dec 27 '14 at 08:45
  • @ManivannanJeganathan I update my answer to use this validation `has_many :tests, :length => { :maximum => 3 }` – Mohamed Yakout Dec 29 '14 at 13:57
  • 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"``` – Manivannan Jeganathan Dec 29 '14 at 14:28
  • 1
    I was trying ```validates_associated :package``` in ```PackageTest``` instead of ```validates_associated :tests``` in Package model. it solved my problem. Thanks for help – Manivannan Jeganathan Dec 30 '14 at 08:24
  • When I calling test_ids in at_most_3_tests method always returns database results instead parameter values. How to retrieve values from memory? – Manivannan Jeganathan Mar 31 '15 at 06:39
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).

Ari
  • 2,311
  • 1
  • 17
  • 17
  • 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] – Manivannan Jeganathan Dec 29 '14 at 13:53
  • I used **validates_associated :package** in PackageTest model – Manivannan Jeganathan Dec 29 '14 at 13:56
  • validates_associated shouldn't raise an exception by default. What error are you getting? – Ari Dec 29 '14 at 15:39
  • ```Validation failed: Package is invalid``` because of parameter test_ids(#=> [1,2,3,4,5]) which is validated in ```Package``` model. – Manivannan Jeganathan Dec 29 '14 at 16:01
  • 3
    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. – Ari Dec 29 '14 at 16:25
  • 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](https://github.com/jmaniv/test_demo) project for your reference. – Manivannan Jeganathan Dec 30 '14 at 08:19