How can I create a parent model only if its children models fit some validations alltogether like the sum of their attributes equal come value, etc?
I have an parent model as:
class Foo < ApplicationRecord
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars, allow_destroy: true
end
And the child model as:
class Bar < ApplicationRecord
belongs_to :foo
end
Is there a right way of doing it? Where should I validate the children models? Also how could I test it with rspec? Like this?
before do
#foo = create(:foo)
#bar = create(:bar, value: 30, foo_id: #foo.id)
end
Rails offers validates_associated, which will ensure the associated records are valid. If the associated records are invalid, the parent record will not be saved.
In Foo:
class Foo < ApplicationRecord
has_many :bars, dependent: :destroy
accepts_nested_attributes_for :bars, allow_destroy: true
validates_associated :bars
end
In Bar:
class Bar < ApplicationRecord
belongs_to :foo
include ActiveModel::Validations
validates_with BarValidator
end
In BarValidator, which is a custom validator:
class BarValidator < ActiveModel::Validator
def validate(record)
record.errors.add :some_error, 'Some message' unless condition_met?
end
end
You've stated:
only if its children models fit some validations alltogether like the sum of their attributes equal come value, etc?
which is somewhat ambiguous. If you truly do need to calculate the sum of children then you can add a validator to the parent which maps through the children and appends an error on failure to meet conditional:
In Foo, (or preferably, a validator):
validate :children_condition
def children_condition
errors[:base] << "Some message" if bars.map(&:attribute).sum != expected_minimum_value
end
Important notes from the documentation on validates_associated:
WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion.
NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association is both present and guaranteed to be valid, you also need to use validates_presence_of.
As for:
Also how could I test it with rspec?
I would create a test suite of valid parent, valid child && valid parent, invalid child, etc and expect the Model.count to have increased by the expected amount (which would be zero in the latter example).
Related
Given some kind of Thing:
class Thing < ApplicationRecord
include CustomFieldable
#...
end
Which can have custom field values attached to it:
module CustomFieldable
extend ActiveSupport::Concern
included do
has_many :custom_field_values, as: :custom_fieldable, dependent: :destroy
validates_associated :custom_field_values
accepts_nested_attributes_for :custom_field_values
end
end
And where custom field values are basically just a string value (at least for now) with a reference to their owner:
class CustomFieldValue < ApplicationRecord
belongs_to :custom_fieldable, polymorphic: true, dependent: :destroy
belongs_to :custom_field, dependent: :destroy
validates_presence_of :custom_fieldable
validates_presence_of :custom_field
validates_presence_of :string_value
end
And to the custom field, which is just a wrapper around a name:
class CustomField < ApplicationRecord
validates_presence_of :name
validates_uniqueness_of :name
end
When I initialise the Thing with a hash:
"thing"=>{
//...other stuff...
"custom_field_values_attributes"=>{
"0"=>{
"custom_field_id"=>"1",
"string_value"=>"value 1"
}
}
}
I would expect ActiveRecord to set up the association from the CustomFieldValue back to the Thing. But it looks like it is not, because I get a validation error:
There were problems with the following fields:
Custom field values custom fieldable can't be blank
Custom field values is invalid
So it's like when I use accepts_nested_attributes_for, the parent association is not set up. Is that expected behaviour?
Update #1:
Controller logic for permitting the fields looks like this:
class ThingController < ApplicationController
def thing_params(action)
common_params = [ omitting common stuff... ]
params.fetch(:licence).permit(*common_params,
custom_field_values_attributes: [
:custom_field_id, :string_value ])
end
end
Update #2:
If I write two tests for the model, I can see the same thing happening.
Fails:
test "adding a custom field value on construction via nested attributes" do
thing = Thing.new custom_field_values_attributes: [
{ custom_field_id: custom_fields(:environment).id,
string_value: 'Testing' }
]
assert_attribute_not_invalid thing, :custom_field_values
assert_equal 'Testing', thing.custom_field_values[0].string_value
end
Passes:
test "adding a custom field value via nested attributes" do
thing = things(:one)
thing.update_attributes custom_field_values_attributes: [
{ custom_field_id: custom_fields(:environment).id,
string_value: 'Testing' }
]
assert_valid thing
assert_equal 'Testing', thing.custom_field_values[0].string_value
end
So it's like, if the record isn't saved yet, Rails doesn't set up the nested models correctly, but if it's already saved, they get set up correctly.
I tried something on a whim. Changed this:
has_many :custom_field_values, as: :custom_fieldable, dependent: :destroy
To this:
has_many :custom_field_values, as: :custom_fieldable, inverse_of: :custom_fieldable, dependent: :destroy
So it seems that Rails cannot guess the inverse relationship for polymorphic associations - even though I was already forced to tell it this using :as. Specifying it twice works fine.
With the code given, I would say every thing works fine. You create a Thing object and add multiple CustomFieldValue objects. The new CustomFieldValue objects have set custom_field_id to 1. This does not mean that Rails loads the custom_field object. This would only happen when you save the object and reload it. So validates_presence_of :custom_field is right to complain. This attribute is still nil. I think the same will happen for custom_fieldable
There are models with has has_many through association:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
validates :categories, presence: true
end
class EventCategory < ActiveRecord::Base
belongs_to :event
belongs_to :category
validates_presence_of :event, :category
end
class Category < ActiveRecord::Base
has_many :event_categories
has_many :events, through: :event_categories
end
The issue is with assigning event.categories = [] - it immediately deletes rows from event_categories. Thus, previous associations are irreversibly destroyed and an event becomes invalid.
How to validate a presence of records in case of has_many, through:?
UPD: please carefully read sentence marked in bold before answering.
Rails 4.2.1
You have to create a custom validation, like so:
validate :has_categories
def has_categories
unless categories.size > 0
errors.add(:base, "There are no categories")
end
end
This shows you the general idea, you can adapt this to your needs.
UPDATE
This post has come up once more, and I found a way to fill in the blanks.
The validations can remain as above. All I have to add to that, is the case of direct assignment of an empty set of categories. So, how do I do that?
The idea is simple: override the setter method to not accept the empty array:
def categories=(value)
if value.empty?
puts "Categories cannot be blank"
else
super(value)
end
end
This will work for every assignment, except when assigning an empty set. Then, simply nothing will happen. No error will be recorded and no action will be performed.
If you want to also add an error message, you will have to improvise. Add an attribute to the class which will be populated when the bell rings.
So, to cut a long story short, this model worked for me:
class Event < ActiveRecord::Base
has_many :event_categories
has_many :categories, through: :event_categories
attr_accessor :categories_validator # The bell
validates :categories, presence: true
validate :check_for_categories_validator # Has the bell rung?
def categories=(value)
if value.empty?
self.categories_validator = true # Ring that bell!!!
else
super(value) # No bell, just do what you have to do
end
end
private
def check_for_categories_validator
self.errors.add(:categories, "can't be blank") if self.categories_validator == true
end
end
Having added this last validation, the instance will be invalid if you do:
event.categories = []
Although, no action will have been fulfilled (the update is skipped).
use validates_associated, official documentaion is Here
If you are using RSpec as your testing framework, take a look at Shoulda Matcher. Here is an example:
describe Event do
it { should have_many(:categories).through(:event_categories) }
end
Take the following for example:
class Foo < AR::Base
has_many :bars, :as => :barable, :dependent=> :destroy
accepts_nested_attributes_for :bars, :allow_destroy => true
end
class Bar < AR::Base
belongs_to :barable, :polymorphic => true
end
class Baz < Bar
before_save do
raise "Hi"
end
end
In the form for 'Foo' - I have fields_for :bars_attributes where a hidden field sets type to 'Baz'. The 'Baz' is succesfully created but the callback never fires. (It does, however, fire when manually creating a 'Baz' in the console.)
Any advice appreciated!
Baz's callbacks will only be triggered if you create it as a Baz object, i.e Baz.new(...).
However, you're not creating a Baz record, but rather a Bar record: Bar.new(type: 'Baz').
This will only trigger Bar's callbacks, even though that later on it will be treated as a Baz.
you need to specify additonal association in your Foo.rb
has_many :bazs
# or
# has_many :bazs class_name: 'ModuleName::Baz' # if you scoped your child classed within some module
If you do that your
before_save do
raise "Hi"
end
will fire on for example #current_user.bazs.build
on a Ruby on Rails project I'm trying to access association objects on an ActiveRecord prior to saving everything to the database.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
def item_ids=(ids)
ids.each do |item_id|
purchase_items.build(item_id: item_id)
end
end
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
If I build out my object like so:
purchase = Purchase.new(item_ids: [1, 2, 3]) and try to save it the item_validation method doesn't have the items collection populated yet, so even though items have been set set it doesn't get a chance to call the check_something method on any of them.
Is it possible to access the items collection before my purchase model and association models are persisted so that I can run validations against them?
If I change my item_validation method to be:
def item_validation
purchase_items.each do |purchase_item|
item = purchase_item.item
## Lookup something with the item
if item.something
errors.add :base, "Error message"
end
end
end
it seems to work the way I want it to, however I find it hard to believe that there is no way to directly access the items collection with rails prior to my purchase and associated records being saved to the database.
Try to adding the argument inverse_of: in the has_many and belongs_to definitions. The inverse_of argument it's the name of the relation on the other model, For example:
class Post < ActiveRecord::Base
has_many :comments, inverse_of: :post
end
class Comment < ActiveRecord::Base
belongs_to :post, inverse_of: :comments
end
Don't forget to add it also on the other classes, such as PurchaseItem and Item
Hope it helps
Remove your own item_ids= method - rails generates one for you (see collection_singular_ids=ids). This might already solve your problem.
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
validate :item_validation
private
def item_validation
items.each do |item|
## Lookup something with the item
if item.check_something
errors.add :base, "Error message"
end
end
end
end
The second thing that comes in my mind looking at your code: Move the validation to the Item class. So:
class Purchase < ActiveRecord::Base
has_many :purchase_items, dependent: :destroy
has_many :items, through: :purchase_items
end
class Item < ActiveRecord::Base
has_many :purchase_items
has_many :purchases, through: :purchase_items
validate :item_validation
private
def item_validation
if check_something
errors.add :base, "Error message"
end
end
end
Your Purchase record will also be invalid if one of the Items is invalid.
Do you have documentation that indicates purchase = Purchase.new(item_ids: [1, 2, 3]) does what you're expecting?
To me that looks like you are just setting the non-database attribute 'item_ids' to an array (i.e. not creating an association).
Your Purchase model should not even have any foreign key columns to set directly. Instead there are entries in the purchase_items table that have a purchase_id and item_id. To create a link between your purchase and the three items you need to create three entries in the joiner table.
What happens if you just do this instead?:
purchase = Purchase.new
purchase.items = Item.find([1,2,3])
You can use model.associations = [association_objects] and an Association Callback http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
I assume you can't access them because id of Purchase isn't available before record saved. But as you mention you have access to first level association purchase_items, so you can extract all ids and pass them in where for Item:
items = Item.where(purchase_item_id: purchase_items.map(&:id))
I want to create user-specific validations.
User has a column called "rule_values" which is a serialized hash of certain quantities.
In a separate model, Foo, I have a validation:
class Foo < ActiveRecord::Base
belongs_to :user
n = self.user.rule_values[:max_awesome_rating] #this line is giving me trouble!
validates_presence_of :awesome_rating, :in => 1..n
end
It seems that self refers to Foo (which is why I'm getting an undefined method error) and not an instance of Foo. How can I access the User instance from within the Foo model?
How about creating a custom validation on Foo something like this?
class Foo < ActiveRecord::Base
validate do |foo|
n = foo.user.rule_values[:max_awesome_rating]
unless (1..n).include? foo.awesome_rating
foo.errors.add :awesome_rating, "must be present and be between 1 and #{n}"
end
end
end
This way you have access to the instance and the user association
Rails supports custom validations (using validate). Here's an idea of how it might work (did not check it though):
class Foo < ActiveRecord::Base
belongs_to :user
validate :awesome_rating_is_in_range
def awesome_rating_is_in_range
errors.add(:awesome_rating, 'not in range') unless (1..user.rule_values[:max_awesome_rating]).include? awesome_rating
end
end