Rails how to validate uniqueness across child objects with nested form - ruby-on-rails

In a nested form, I want the user to have the ability to create or modify all of a Parent's Childs at one time. So let's so the params that are passed are like this:
{"childs_attributes" => [{attribute:1}, {attribute:2}, {attribute:3}...]}
I would like a validation that says for any one Parent, the attributes of all of its Childs must be unique. In other words, in the above example, that's OK because you'd get:
Parent.childs.pluck(:attribute).uniq.length == Parent.childs.pluck(:attribute).length
However, if the params passed were like below, it'd be a violation of the validation rule:
{"childs_attributes" => [{attribute:1}, {attribute:2}, {attribute:3}...]}
So far the only solution I've come up with to do this validation is in the Controller... which I know is bad practice because we want to push this to the model.
The problem is that if in the model I have something like the below:
class Parent
validate :unique_attribute_on_child
def unique_attribute_on_child
attribute_list = self.childs.pluck(:attribute)
if attribute_list.uniq.length != attribute_list.length
errors[:base] << "Parent contains Child(s) with same Attribute"
end
end
end
That won't work because self.childs.pluck(:attribute) won't return the attribute passed in the current update, since the current update won't have saved yet.
I guess I could do something like an after_save but that feels really convoluted since it's going back and reversing db commits (not to mention, the code as written below [non tested, just an example] likely leads to a circular loop if I'm not careful, since Parent validate associated children)
after_save :unique_attribute_on_child
def unique_attribute_on_child
attribute_list = self.childs.pluck(:attribute)
if attribute_list.uniq.length != attribute_list.length
self.childs.each { |c| c.update_attributes(attribute:nil) }
errors[:base] << "Parent contains Child(s) with same Attribute"
end
end
end
Other ideas?

My first impulse is to suggest that Rails uses smart pluralization and to try using children instead of childs, but I think this was just the example.
I now recommend that you change your strategy. Call the validation on the children like so:
class Child < ActiveRecord::Base
belongs_to :parent
...
validates :child_attribute, uniqueness: { scope: :parent }
...
end

Related

Rails 5 set has_one association with strong params

I am developing a Rails 5 application in which I encountered the following difficulty.
I've got two models, let's say Kid and Toy, which are in one-to-one relationship like this:
class Kid < ActiveRecord::Base
has_one :toy
end
class Toy
belongs_to :kid, optional: true
end
So the toys can belong to zero or one kid, and from day to day it can change - it is always another kid's responsibility to look after a certain toy. Now, when I edit a toy, changing its kid is easy as can be: I just send kid_id in the strong params to update the record:
params.require(:toy).permit(:name, :type, :kid_id)
But recently, I was asked to implement the changing feature from the other way too, that is, when editing a kid, I should do something like this:
params.require(:kid).permit(:name, :age, :toy_id)
The problem is that - while belongs_to works with association_id and even has_many provides association_ids getter and setter - has_one relationship has nothing like this. What is more, has_one association gets saved the moment I call association = #record. So I simply cannot set it by sending the toy_id in the strong parameters.
I could do something like #kid.update(kid_params); #kid.toy = #toy on the controller level, but that would rather bring model logics to my controller, not to mention that I want to check if the newly assigned toy did not belong to another kid, which I imagine as some kind of validation.
The best I could come up with was to define some rails-like methods for Kid class like
def toy_id
#toy_id = toy.id unless defined?(#toy_id)
#toy_id
end
def toy_id_changed?
toy_id != toy.id
end
and set a validation and a before_commit callback
validate if: -> { toy_id.present? && toy_id_changed? } do
errors.add :toy_id, :other_has_it if new_toy.kid_id.present? && new_toy.kid_id != id
end
before_commit if: -> { toy_id_changed? } do
toy = new_toy
end
private
def new_toy
#new_toy ||= Toy.find(toy_id)
end
So far it works as expected, and now I can send toy_id in the strong params list to update a kid, and it updates the toy association if -
and only if - there is no validation error. I have even put it in a concern to be nice and separated.
My question is: isn't there a rails way to do this? haven't I reinvented the wheel?
Thanks in advance!

Updating association without saving it

I have a model:
class A < ActiveRecord::Base
has_many :B
end
And I want to reset or update A's B association, but only save it later:
a = A.find(...)
# a.bs == [B<...>, B<...>]
a.bs = []
#or
a.bs = [B.new, B.new]
# do some validation stuff on `a` and `a.bs`
So there might be some case where I will call a.save later or maybe not. In the case I don't call a.save I would like that a.bs stay to its original value, but as soon as I call a.bs = [], the old associations is destroyed and now A.find(...).bs == []. Is there any simple way to set a record association without persisting it in the database right away? I looked at Rails source and didn't find anything that could help me there.
Thanks!
Edit:
I should add that this is for an existing application and there are some architecture constraint that doesn't allow us to use the the regular ActiveRecord updating and validation tools. The way it works we have a set of Updater class that take params and assign the checkout object the value from params. There are then a set of Validater class that validate the checkout object for each given params. Fianlly, if everything is good, we save the model.
In this case, I'm looking to update the association in an Updater, validate them in the Validator and finally, persist it if everything check out.
In summary, this would look like:
def update
apply_updaters(object, params)
# do some stuff with the updated object
if(validate(object))
object.save(validate: false)
end
Since there are a lot of stuff going on between appy_updaters and object.save, Transaction are not really an option. This is why I'm really looking to update the association without persisting right away, just like we would do with any other attribute.
So far, the closest solution I've got to is rewriting the association cache (target). This look something like:
# In the updater
A.bs.target.clear
params[:bs].each{|b| A.bs.build(b)}
# A.bs now contains the parameters object without doing any update in the database
When come the time to save, we need to persist cache:
new_object = A.bs.target
A.bs(true).replace(new_object)
This work, but this feel kind of hack-ish and can easily break or have some undesired side-effect. An alternative I'm thinking about is to add a method A#new_bs= that cache the assigned object and A#bs that return the cached object if available.
Good question.
I can advice to use attributes assignment instead of collection manipulation. All validations will be performed as regular - after save or another 'persistent' method. You can write your own method (in model or in separated validator) which will validate collection.
You can delete and add elements to collection through attributes - deletion is performed by additional attribute _destroy which may be 'true' or 'false' (http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html), addition - through setting up parent model to accept attributes.
As example set up model A:
class A < ActiveRecord::Base
has_many :b
accepts_nested_attributes_for :b, :allow_destroy => true
validates_associated :b # to validate each element
validate :b_is_correct # to validate whole collection
def b_is_correct
self.bs.each { |b| ... } # validate collection
end
end
In controller use plain attributes for model updating (e.g update!(a_aparams)). These methods will behave like flat attribute updating. And don't forget to permit attributes for nested collection.
class AController < ApplicationController
def update
#a = A.find(...)
#a.update(a_attributes) # triggers validation, if error occurs - no changes will be persisted and a.errors will be populated
end
def a_attributes
params.require(:a).permit([:attr_of_a, :b_attributes => [:attr_of_b, :_destroy]])
end
end
On form we used gem nested_form (https://github.com/ryanb/nested_form), I recommend it. But on server side this approach uses attribute _destroy as mentioned before.
I finally found out about the mark_for_destruction method. My final solution therefor look like:
a.bs.each(&:mark_for_destruction)
params[:bs].each{|b| a.bs.build(b)}
And then I can filter out the marked_for_destruction? entry in the following processing and validation.
Thanks #AlkH that made me look into how accepts_nested_attributes_for was working and handling delayed destruction of association.

How do I change an ActiveRecord from marked to be saved to make sure it does not get saved, from within the model itself?

How do I change an AcriveRecord from marked to be saved to make sure it does not get saved, from within the model itself?
Considering I can have a method run by a hook in activerecord, such as: before_save
for (hypothetical) example:
before_save :ignore_new_delete_exisiting_if_blank(self.attribute)
def ignore_new_delete_exisiting_if_blank(attribute)
self.do_not_save_me! if attribute.blank?
#what is that magic "do_not_save_me" method?
#Is there such thing, or something to achieve the same thing?
end
Update
My particular use case requires that no errors be thrown and other models to continue to be saved, even if this one will not. I should explain:
I am using model inheritance, and I am having an issue with figuring out how to let save the parent model, but if the child model instances are blank, (no values exist in certain attributes) they should not be persisted; however, the parent should still be persisted. This scenario does not let me make use of validations on the child model as that would block the parent from being persisted as well...
Your method should just return false to make it does not save.
Or you set the errors, which will allow to be more descriptive.
For example:
def ignore_new_delete_exisiting_if_blank_attribute
if attribute.blank?
errors.add(:base, "Not allowed to save if attribute is blank.")
end
end
Note that you cannot send parameters to a before_save. If you just want to make sure a record is not saved when an attribute is not present, you should use
validates_presence_of :attribute
[UPDATE]
When saving a parent model with children, you have to do something like accepts_nested_attributes_for, and in that call, you can specify which attributes must be given or when a child-record is ignored.
For example
accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
will not save a post if the title is blank.
Hope this helps.
The "magic" is that when you return false from the method, the record won't be saved.
In your case:
def ignore_new_delete_exisiting_if_blank(attribute)
attribute.present?
end

Rails: hook for validating model when it's appended to its parent resource?

I have a situation where a user submits a multi-model form. Through that form, I have to assign the user a random item from my database along to their account. As I loop through the data, I flag the item that I've assigned to them with a boolean in_use flag. The problem is, in a situation such as the following:
2.times do |n|
# grab random item which will be parent
parent = # some random code to grab an item not in use
parent.in_use = 1
parent.child.build
children << child
end
Three models involved here. The parent itself is within its own parent, hence the children << child statement. The problem here is an edge condition is that the code that grabs a random item not in use can grab the same parent twice as I don't know of a hook that will allow me to save parent.in_use after the child has been appended to its parent via children << child. The loop will go again, the in_use flag hasn't been persisted to the database and it can select it again. Is there a way to persist it, then roll it back if validation fails in a situation like this?
Does this work?
2.times do |n|
# grab random item which will be parent
parent = # some random code to grab an item not in use
parent.child.build
parent.in_use
parent.save # <--- save the parent on the db
children << child
end
I'm not sure about the last line, since you didn't explain what the children variable was. I'm also assuming that parent.in_use is a method, not a property (otherwise you would have to write parent.in_use = true or something similar)
One more thing - it seems you are using an external attribute (called in_use or similar) in order to store whether a parent has children. It would be probably simpler just to count the number of children. There are several ways to do this, but a good compromise is using an automatic counter cache.
class Parent << ActiveRecord::Base
has_many :children, :counter_cache => true #this will had a children_count attribute
The attribute is named just like the children table. So if you write has_many :issues, the counter will be issues_Count.
You will have to add a children_count attribute to the parent's table.
You then can do if parent.children_count > 0 instead of checking strange parent.in_use attributes.
More information on the counter cache on railscast#23
Your question is frustrating, because you say your issue stems from pulling in the same parent twice, but the code to pull the parent is omitted. Yet the code for an irrelevant confusing contextless 3 model relationship is left in.
Anyway, I'm going to say this how it seems from my perspective, and I think you'll agree:
The solution you are asking for has you getting the same parent twice (now in two places in memory with different state between them) building relationships that shouldn't exist, somehow figuring this out in your validations, having a conditional in the loop to check for it and retry the whole process.
From my perspective, this sounds like a nightmare. As someone who has written horribly convoluted bug inclined code like that, I strongly recommend you just get two different parents in the first place. Don't let errors propagate across your app, contain them early, or better yet, prevent them from happening at all:
class YourUnnamedModel < ActiveRecord::Base
named_scope :unused , :conditions => { :in_use => false }
named_scope :random , :order => 'RANDOM()'
named_scope :limited , lambda { |n=1| return :limit => n }
def self.example
unused.random.limited(2).each do |parent|
puts "doing stuff with #{parent.inspect}"
end
nil
end
end
Caveat: For some stupid reason (https://rails.lighthouseapp.com/projects/8994/tickets/1274-patch-add-support-for-order-random-in-queries), it is possible that your 'RANDOM()' could be called something else.

How can I force valadition to occur when appending an object to another object in Rails?

In one of my model objects I have an array of objects.
In the view I created a simple form to add additional objects to the array via a selection box.
In the controller I use the append method to add user selected objects to the array:
def add_adjacents
#site = Site.find(params[:id])
if request.post?
#site.adjacents << Site.find(params[:adjacents])
redirect_to :back
end
end
I added a validation to the model to validate_the uniqueness_of :neighbors but using the append method appears to be bypassing the validation.
Is there a way to force the validation? Or a more appropriate way to add an element to the array so that the validation occurs? Been googling all over for this and going over the books, but can't find anything on this.
Have you tried checking the validity afterwards by calling the ".valid?" method, as shown below?
def add_adjacents
#site = Site.find(params[:id])
#site.neighbors << Site.find(params[:neighbors])
unless #site.valid?
#it's not valid, do something to fix it!
end
end
A couple of comments:
Then only way to guarantee uniqueness is to add a unique constraint on your database. validates_uniqueness_of has it's gotchas when there are many users in the system:
Process 1 checks uniqueness, returns true.
Process 2 checks uniqueness, returns true.
Process 1 saves.
Process 2 saves.
You're in trouble.
Why do you have to test for request.post?? This should be handled by your routes, so in my view it's logic that is fattening your controller unnecessarily. I'd imagine something like the following in config/routes.rb: map.resources :sites, :member => { :add_adjacents => :post }
Need to know more about your associations to figure out how validates_uniqueness_of should play in with this setup...
I think you're looking for this:
#site.adjacents.build params[:adjacents]
the build method will accept an array of attribute hashes. These will be validated along with the parent model at save time.
Since you're validating_uniqueness_of, you might get some weirdness when you are saving multiple conflicting records at the same time, depending on the rails implementation for the save and validation phases of the association.
A hacky workaround would be to unique your params when they come in the door, like so:
#site.adjacents.build params[:adjacents].inject([]) do |okay_group, candidate|
if okay_group.all? { |item| item[:neighbor_id] != candidate[:neighbor_id] }
okay_group << candidate
end
okay_group
end
For extra credit you can factor this operation back into the model.

Resources