Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 529
Manivannan Jeganathan Avatar asked Oct 28 '14 15:10

Manivannan Jeganathan


People also ask

How do I validate in Ruby on Rails?

This helper validates the attributes' values by testing whether they match a given regular expression, which is specified using the :with option. Alternatively, you can require that the specified attribute does not match the regular expression by using the :without option. The default error message is "is invalid".

Can we save an object in DB if its validations do not pass?

If any validations fail, the object will be marked as invalid and Active Record will not perform the INSERT or UPDATE operation. This helps to avoid storing an invalid object in the database. You can choose to have specific validations run when an object is created, saved, or updated.

What is validate in rails?

In Rails, validations are used in order to ensure valid data is passed into the database. Validations can be used, for example, to make sure a user inputs their name into a name field or a username is unique.


2 Answers

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
like image 166
Mohamed Yakout Avatar answered Nov 15 '22 08:11

Mohamed Yakout


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

like image 20
Ari Avatar answered Nov 15 '22 08:11

Ari