rails : validation vs reject_if in nested forms - ruby-on-rails

I have a model A which accepts nested attributes for model B. The number of model B entries and whether an entry is required or not is decided based on a 3rd table in db. I use form_for to generate a form with fields_for for model B entries. This is how the validation needs to work:
An entry for model B is allowed to be blank if its corresponding :required field is false, but it should not be saved, i.e, this entry should not raise validation error but should be rejected by :reject_if.
An entry whose :required field is true is not allowed to be blank. These entries should not be rejected by the :reject_if but raise a validation error when it is being saved to db.
class modelA < ApplicationRecord
has_many :modelBs, dependent: :destroy
accepts_nested_attributes_for :modelBs, reject_if: :not_required_and_blank?
def not_required_and_blank?
# return true if modelB.entry is blank && modelB.required_entry? is false
end
end
class modelB < ApplicationRecord
belongs_to :modelA
validates :modelB_entry, presence: true, if: :required_entry?
def required_entry?
ModelC.find(:id).required
end
end
Should I do this
in the :reject_if option of accepts_nested_attributes_for (in modelA class) and in validates_presence_of modelB_entry method (in modelB class) OR
everything in :reject_if OR
everything in :validates_presence_of ?
Which gets executed first :reject_if or vaildation?

reject_if and validations have different purpose.
suppose you have a nested model modelB with two fields name and title, with name as mandatory field.
Use of reject_if
In the controller #new action, you decide to build the first nested object yourself to give user a better UI experience so that he can easily see what else he can add to the form. or the user himself clicks on the "+ Add more" button. But then decides to leave modelB blank and not entering any info in it. Now, if you don't use reject_if but you have validations the user will get the error message saying field is blank, and he won't be able to submit the form, unless he clicks on remove button.
So, in this case you will check if all fields are blank in the reject_if, if that is the case we can ignore the record, else let the model do it's work.
Also, keep in mind that reject_if does not guarantees data integrity. As any typical RoR app has multiple entry points.
In my opinion, you should go with the first option. Use both reject_if and validations

Related

How to avoid saving nil/blank in accepts_nested_attributes_for when displaying set number of fields for has_many association?

My question is an edgecase of how to avoid saving empty records on a nested rails form. I have a simple has_many, where a user can have a maximum of 5 job titles.
# user.rb
has_many :job_titles
validates_length_of :job_titles, maximum: 5
accepts_nested_attributes_for :job_titles,
allow_destroy: true,
:reject_if => proc { |att| att[:name].blank? }
# job_titles.rb
belongs_to :user
validates_associated :user
The proc should remove any blanks, but they get created anyway (!) since I have this in the users_controller, which is used to ensure there are always 5 form fields presented in the view:
# users_controller.rb
num_job_titles = #user.job_titles.count
(5-num_job_titles).times { #user.job_titles.build }
With this, the blanks keep appearing in the database even before the form is submitted, since the previous code builds those blank records, and the model validation seems to allow it for some reason - I didn't expect it to.
Question
How can I ensure 5 fields are displayed for 5 different associated records (job titles), and ensure blank job titles aren't saved as records to the database?

Skip saving associated object if invalid on update

I have a person table and address table like this:
class Person < ApplicationRecord
has_many :addresses, dependent: :destroy
accepts_nested_attributes_for :addresses
end
class Address < ApplicationRecord
belongs_to :person
validates :address_line_1, presence: true
end
In my controller, I want to update the person and the associated addresses, but if the address is invalid and the person is valid, I'd still want to update the Person object and keep the invalid address the same as before without running into a ROLLBACK.
What's the best way to handle this? I realize I can do some logic to check if the address is invalid and remove the addresses_attributes from the parameters, then assign the parameters again and save it, but is there any better way?
has_many association has a validate option you can set to false and handle validations however you want https://guides.rubyonrails.org/association_basics.html#options-for-has-many-validate
I think you are using accepts_nested_attributes_for since you named the addresses_attributes param, personally I wouldn't combine no validation with that, you may end up with invalid addresses.
Personally, I would do two step (with the default validate: true config):
first update only the user's attributes
call save on the user (so addresses doesn't mess the update up)
set the addresses attributes
call save on the user (so everything gets validated again)
EDIT: if you want to use the validate: false option you may want to set autosave: false too so you don't save invalid addresses https://guides.rubyonrails.org/association_basics.html#options-for-has-many-autosave
Ultimately you'll either want to check that the address attributes are valid and remove them if not OR saved the records separately.
A common pattern is to opt for second option using a form object. This helps keep the logic out of the controller and makes it easier to expand on updating a Person in the future.

Validating a conditionally displayed field in Rails

I have a Competition and a Competition Entry model, the former includes a form and an optional "Question" field which isn't displayed if the admin user doesn't fill it out.
The Competition Entry model includes an "Answer" field that only needs to be validated if the question is present, but I'm not sure how to achieve that - is there a way to take advantage of the belongs_to/has_many association they have?
You can make validations conditional on a method, and in that method check the associated model.
class CompetitionEntry < ActiveRecord::Base
validates :answer, :presence => true, :if => :validate_answer?
def validate_answer?
!self.competition.question.blank?
end
end
A railscast about conditional validations!

how to avoid saving empty records on a nested rails form

I'm using the nested_form gem for my AddressBook relation. When the user blanks out the value of an existing Addr, I want to delete that Addr rather than saving with a blank value
class Person < ActiveRecord::Base
has_many :addrs, dependent: :destroy
attr_accessible :name, :addrs_attributes
accepts_nested_attributes_for :addrs, reject_if: :addr_blank, allow_destroy: true
def addr_blank(a)
valid? && a[:id].blank? && a[:value].blank?
end
class Addr < ActiveRecord::Base
belongs_to :person
attr_accessible :kind, :label, :value, :person_id
My :reject_if method works well but it doesn't give me everything I need
valid? keeps my blank Addrs around through validation
a[:id].blank? avoids rejections when the user blanks out and existing record
Now, I need to delete (rather than save) an existing Addr when the user blanks the value. Also, I'm exposing Persons and Addrs via a RESTful API. I see two possible options:
Post process the params hash to add the magical _destroy=1 param. IOW, emulate the user activity of pressing the delete button.
Encapsulate this inside the Addr model such that an update with a blank value is effectively considered a delete.
Based on the advice here is how I implemented it:
people_controller.rb
def update
#person = Person.find(params[:id])
#person.destroy_blank_addrs(params[:person])
respond_to do |format|
...
person.rb
def destroy_blank_addrs(person_params)
if valid? && person_params[:addrs_attributes]
person_params[:addrs_attributes].each do |addr_params_array|
addr_params= addr_params_array[1]
addr_params[:_destroy] = '1' if !addr_params[:id].blank? && addr_params[:value].blank?
end
end
end
accepts_nested_attributes_for :addrs,
allow_destroy: true,
:reject_if => proc { |att| att[:name].blank? && attr[:description].blank? }
accepts_nested_attributes_for :addrs,
allow_destroy: true,
reject_if: -> { |attr| [name, description].any? &:blank? }
A third alternative would be to add an before_save callback on Person that will remove all addresses that are blank. This idea has some merit, but I probably won't go with it.
Out of the two options you present, I will not go with post-processing the params. It will work out, but it is too much work. Besides, to controller code will get a bit messier and I'm a firm believer in a very slim controller.
The easiest option, in my head, is to remove the blank addresses after saving. You can add Person#remove_blank_addresses() and then call it on successful save. You don't need to pass in the params - it can just iterate the addresses and remove the blank ones. It has the disadvantage of creating empty addresses and then destroying them, but you would need that for updating people anyway.
If we're talking about the cleanest solution (in my opinion), I would introduce a third class that would handle all that logic and have the controller delegate to it. The controller would be easy enough to test in isolation and then you can write a model spec that checks all the nitty-gritty details. It is a bit more work and I can't think of a good name right now (PersonUpdater?), but it might be an idea worth thinking about.
accepts_nested_attributes_for :addrs,
allow_destroy: true,
reject_if: :all_blank

Nested attribute validations not being called

I've been struggling with this for some time now. I'm just trying to get nested attributes to validate on Rails 3.2 with no luck. It's like it's just completely ignoring validations for the nested attributes. Below is an example validation that's not working:
class Invoice < ActiveRecord::Base
validates :description, :presence => true
belongs_to :client_branch
has_many :invoice_items
accepts_nested_attributes_for :invoice_items, :allow_destroy => true
end
class InvoiceItem < ActiveRecord::Base
belongs_to :invoice
validate :thisisatest
def thisisatest
errors.add(:qty, 'QTY NOT VALIDATING TEST.')
end
end
When saving an Invoice with some InvoiceItems, it saves it successfully, even though the custom validation is clearly adding an error for the :qty attribute. Is there something I should be adding to my models for nested validation to work, or am I perhaps missing something else?
Actually, I'm being daft. I changed the model name, along with all references to it and it took me this long to miss one reference still pointing to the old model in the javascript. Thus, items being added dynamically weren't named correctly, causing the validation not to trigger. :/

Resources