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.
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".
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.
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.
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
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With