I'm having trouble accessing validation messages for a related model when saving. The setup is that a "Record" can link to many other records via a "RecordRelation" which has a label stating what that relation is, e.g. that a record "refers_to" or "replaces" another:
class Record < ApplicationRecord
has_many :record_associations
has_many :linked_records, through: :record_associations
has_many :references, foreign_key: :linked_record_id, class_name: 'Record'
has_many :linking_records, through: :references, source: :record
...
end
class RecordAssociation < ApplicationRecord
belongs_to :record
belongs_to :linked_record, :class_name => 'Record'
validates :label, presence: true
...
end
Creating the record in the controller looks like this:
def create
# Record associations must be added separately due to the through model, and so are extracted first for separate
# processing once the record has been created.
associations = record_params.extract! :record_associations
#record = Record.new(record_params.except :record_associations)
#record.add_associations(associations)
if #record.save
render json: #record, status: :created
else
render json: #record.errors, status: :unprocessable_entity
end
end
And in the model:
def add_associations(associations)
return if associations.empty? or associations.nil?
associations[:record_associations].each do |assoc|
new_association = RecordAssociation.new(
record: self,
linked_record: Record.find(assoc[:linked_record_id]),
label: assoc[:label],
)
record_associations << new_association
end
end
The only problem with this is if the created association is somehow incorrect. Rather than seeing the actual reason, the error I get back is a validation for the Record, i.e.
{"record_associations":["is invalid"]}
Can anyone suggest a means that I might get record_association's validation back? This would be useful information for a user.
For your example, I would rather go with nested_attributes. Then you should easily get access to associated record errors. An additional benefit of using it is removing custom logic you have written for such behavior.
For more information check documentation - https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Related
Creating a record with nested associations fails to save any of the associated records if one of the records fails validations.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
accepts_nested_attributes_for :episodes
end
class Episode < ActiveRecord::Base
belongs_to :podcast, inverse_of: :episodes
validates :podcast, :some_attr, presence: true
end
# Creates a podcast with one episode.
case_1 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
]
}
# Creates a podcast without any episodes.
case_2 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
{title: "ep2"} # <- Invalid Episode
]
}
I'd expect case_1 to save successfully with one created episode.
I'd expect case_2 to do one of two things:
Save with one episode
Fail to save with validation errors.
Instead the podcast saves but neither episode does.
I'd like the podcast to save with any valid episode saved as well.
I thought to reject invalid episodes by changing the accepts nested attributes line to
accepts_nested_attributes_for :episodes, reject_if: proc { |attributes| !Episode.new(attributes).valid? }
but every episode would be invalid because they don't yet have podcast_id's, so they would fail validates :podcast, presence: true
Try this pattern: Use the :reject_if param in your accepts_nested_attributes_for directive (docs) and pass a method to discover if the attributes are valid. This would let you offload the validation to the Episode model.
Something like...
accepts_nested_attributes_for :episodes, :reject_if => :reject_episode_attributes?
def reject_episode_attributes?( attributes )
!Episode.attributes_are_valid?( attributes )
end
Then in Episode you make a method that tests those however you like. You could even create a new record and use existing validations.
def self.attributes_are_valid?( attributes )
new_e = Episode.new( attributes )
new_e.valid?
end
You can use validates_associated to cause the second option (Fail to save with validation errors)
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
validates_associated :episodes
accepts_nested_attributes_for :episodes
end
UPDATE:
To do option one (save with one episode) you could do something like this:
1. Add the validates_associated :episodes
2. Add code in your controller's create action after the save of #podcast fails. First, inspect the #podcast.errors object to see if the failure is caused by validation error of episodes (and only that) otherwise handle as normal. If caused by validation error on episodes then do something like #podcast.episodes.each {|e| #podcast.episodes.delete(e) unless e.errors.empty?} Then save again.
This would look something like:
def create
#podcast = Podcast.new(params[:podcast])
if #podcast.save
redirect_to #podcast
else
if #some conditions looking at #podcast.errors to see that it's failed because of the validates episodes
#podcast.episodes.each do |episode|
#podcast.episodes.delete(episode) unless episode.errors.empty?
end
if #podcast.save
redirect_to #podcast
else
render :new
end
else
render :new
end
end
end
To get the first option, try turning autosaving on for your episodes.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast, autosave: true
accepts_nested_attributes_for :episodes
end
Suppose I have the following models with nested attributes.
Request model:
class User::Request < ApplicationRecord
## Associations
has_many :items, dependent: :destroy
accepts_nested_attributes_for :items
end
Item model:
class Item < ApplicationRecord
belongs_to :request, class_name: 'User::Request'
has_one :cart_item, dependent: :destroy
has_one :cart, through: :cart_item
accepts_nested_attributes_for :cart_item, allow_destroy: true
end
Cart Item Model:
class CartItem < ApplicationRecord
belongs_to :cart
belongs_to :item
validate :validate_no_duplicate_items, on: :create
private
def validate_no_duplicate_items
return unless cart.items.exists?(item.id)
errors.add(:error, 'Item already added to cart')
end
end
And a controller that will create a request with the multi-level nested attributes.
def generate_checkout
request = User::Request.new(request_params_with_nested_attribuets)
if request.save
render json: request, status: :created
else
render json: request.errors, status: :bad_request
end
end
so the expected behaviour is that if it failed the validation in the cart_item model it will rollback and not create any of the models. But what I get is that the validation block is entered after the cart_item model is saved. Would appreciate any help I can get
Try Active Record Transactions
Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action. The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and vice versa. Transactions enforce the integrity of the database and guard the data against program errors or database break-downs. So basically you should use transaction blocks whenever you have a number of statements that must be executed together or not at all.
def generate_checkout
ActiveRecord::Base.transaction do
request = User::Request.new(request_params_with_nested_attribuets)
if request.save
render json: request, status: :created
else
render json: request.errors, status: :bad_request
end
end
end
I have models:
class Agency < ActiveRecord::Base
SPECIALIZATIONS_LIMIT = 5
has_many :specializations
has_many :cruise_lines, through: :specializations
validate :validate_specializations_limit
protected
def validate_specializations_limit
errors.add(:base, "Agency specializations limit is #{SPECIALIZATIONS_LIMIT}.") if specializations.count > SPECIALIZATIONS_LIMIT
end
end
class CruiseLine < ActiveRecord::Base
has_many :specializations
has_many :agencies, through: :specializations
end
class Specialization < ActiveRecord::Base
belongs_to :agency, inverse_of: :specializations
belongs_to :cruise_line, inverse_of: :specializations
end
In my service I try to save agency and specialization relations like this:
attributes = params.require(:agency).permit(
:name, :website, :description, :booking_email, :booking_phone,
:optional_booking_phone, :working_hours, :cruise_line_ids => []
)
agency.update_attributes(attributes)
attributes[:cruise_line_ids].select{|x| x.to_i > 0}.each do |cruise_line_id|
agency.specializations.build(cruise_line_id: cruise_line_id)
end
How this code works:
1) it updates attributes;
2) it saves relations and updates them if I deselect some checkboxes with cruise lines;
3) in case I enter incorrect data or select too many cruise line checkboxes, it does not save data, but also shows no errors!
If I add some inspect code puts agency.errors.inspect immediately AFTER the block that builds relations - it works exactly as I expect: if everything saved - shows success message, if validation errors occured - shows error messages.
QUESTION:
why adding puts agency.errors.inspect immediately after saving code makes everything work as expected?
I've found bug: it was in my controller, that made redirect to edit page always after storing method worked (even if model was not saved!). Controller code looks like this:
if AgencyService.update_agency(current_agent.agency, params)
flash[:success] = "Agency data updated successfully"
redirect_to action: :edit
else
render :edit
end
The only thing I had to do was to add this code to the end of my update_agency method
# ... build relations
return false if agency.errors.present?
true
end
So if my AgencyService.update_agency return false - user is not redirected and all errors are shown on the page :)
Creating a record with nested associations fails to save any of the associated records if one of the records fails validations.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
accepts_nested_attributes_for :episodes
end
class Episode < ActiveRecord::Base
belongs_to :podcast, inverse_of: :episodes
validates :podcast, :some_attr, presence: true
end
# Creates a podcast with one episode.
case_1 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
]
}
# Creates a podcast without any episodes.
case_2 = Podcast.create {
title: 'title'
episode_attributes: [
{title: "ep1", some_attr: "some_attr"}, # <- Valid Episode
{title: "ep2"} # <- Invalid Episode
]
}
I'd expect case_1 to save successfully with one created episode.
I'd expect case_2 to do one of two things:
Save with one episode
Fail to save with validation errors.
Instead the podcast saves but neither episode does.
I'd like the podcast to save with any valid episode saved as well.
I thought to reject invalid episodes by changing the accepts nested attributes line to
accepts_nested_attributes_for :episodes, reject_if: proc { |attributes| !Episode.new(attributes).valid? }
but every episode would be invalid because they don't yet have podcast_id's, so they would fail validates :podcast, presence: true
Try this pattern: Use the :reject_if param in your accepts_nested_attributes_for directive (docs) and pass a method to discover if the attributes are valid. This would let you offload the validation to the Episode model.
Something like...
accepts_nested_attributes_for :episodes, :reject_if => :reject_episode_attributes?
def reject_episode_attributes?( attributes )
!Episode.attributes_are_valid?( attributes )
end
Then in Episode you make a method that tests those however you like. You could even create a new record and use existing validations.
def self.attributes_are_valid?( attributes )
new_e = Episode.new( attributes )
new_e.valid?
end
You can use validates_associated to cause the second option (Fail to save with validation errors)
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast
validates_associated :episodes
accepts_nested_attributes_for :episodes
end
UPDATE:
To do option one (save with one episode) you could do something like this:
1. Add the validates_associated :episodes
2. Add code in your controller's create action after the save of #podcast fails. First, inspect the #podcast.errors object to see if the failure is caused by validation error of episodes (and only that) otherwise handle as normal. If caused by validation error on episodes then do something like #podcast.episodes.each {|e| #podcast.episodes.delete(e) unless e.errors.empty?} Then save again.
This would look something like:
def create
#podcast = Podcast.new(params[:podcast])
if #podcast.save
redirect_to #podcast
else
if #some conditions looking at #podcast.errors to see that it's failed because of the validates episodes
#podcast.episodes.each do |episode|
#podcast.episodes.delete(episode) unless episode.errors.empty?
end
if #podcast.save
redirect_to #podcast
else
render :new
end
else
render :new
end
end
end
To get the first option, try turning autosaving on for your episodes.
class Podcast < ActiveRecord::Base
has_many :episodes, inverse_of: :podcast, autosave: true
accepts_nested_attributes_for :episodes
end
I am developing a Rails 3.2 application with the following models:
class User < ActiveRecord::Base
# Associations
belongs_to :authenticatable, polymorphic: true
# Validations
validates :authenticatable, presence: true # this is the critical line
end
class Physician < ActiveRecord::Base
attr_accessible :user_attributes
# Associations
has_one :user, as: :authenticatable
accepts_nested_attributes_for :user
end
What I am trying to do is validate whether a user always has an authenticatable parent. This works fine in itself, but in my form the user model complains that the authenticatable is not present.
I am using the following controller to show a form for a new physician which accepts nested attributes for the user:
def new
#physician = Physician.new
#physician.build_user
respond_to do |format|
format.html # new.html.erb
format.json { render json: #physician }
end
end
And this is my create method:
def create
#physician = Physician.new(params[:physician])
respond_to do |format|
if #physician.save
format.html { redirect_to #physician, notice: 'Physician was successfully created.' }
format.json { render json: #physician, status: :created, location: #physician }
else
format.html { render action: "new" }
format.json { render json: #physician.errors, status: :unprocessable_entity }
end
end
end
On submitting the form, it says that the user's authenticatable must not be empty. However, the authenticatable_id and authenticatable_type should be assigned as soon as #physician is saved. It works fine if I use the same form to edit a physician and its user, since then the id and type are assigned.
What am I doing wrong here?
I believe this is expected:
https://github.com/rails/rails/issues/1629#issuecomment-11033182 ( last two comments).
Also check this out from rails api:
One-to-one associations
Assigning an object to a has_one association automatically saves that
object and the object being replaced (if there is one), in order to
update their foreign keys - except if the parent object is unsaved
(new_record? == true).
If either of these saves fail (due to one of the objects being
invalid), an ActiveRecord::RecordNotSaved exception is raised and the
assignment is cancelled.
If you wish to assign an object to a has_one association without
saving it, use the build_association method (documented below). The
object being replaced will still be saved to update its foreign key.
Assigning an object to a belongs_to association does not save the
object, since the foreign key field belongs on the parent. It does not
save the parent either.
and this
build_association(attributes = {}) Returns a new object of the
associated type that has been instantiated with attributes and linked
to this object through a foreign key, but has not yet been saved.
You have to create a Parent first. Then assign it's id to polymorphic object.
From what I can see, you create an object Physician.new which builds User but at this point it's not saved yet, so it doesn't have an id, so there is nothing to assign to polymorphic object. So validation will always fail since it's called before save.
In other words: In your case when you call build_user, it returns User.new NOT User.create . Therefore authenticatable doesn't have a authenticatable_id assigned.
You have several options:
Save associated user first.
OR
Move validation in to after_save callback ( Possible but very annoying and bad)
OR
Change your app structure - maybe avoid polymorphic association and switch to has_many through? Hard for me to judge since I don't know internals and business requirements. But it seems to me this is not a good candidate for polymorphic association. Will you have more models than just User that will be authenticatable?
IMHO the best candidates for polymorphic associations are things like Phones, Addresses, etc. Address can belong to User, Customer, Company, Organization, Area51 etc, be Home, Shipping or Billing category i.e. It can MORPH to accommodate multiple uses, so it's a good object to extract. But Authenticatable seems to me a bit contrived and adds complexity where there is no need for it. I don't see any other object needing to be authenticable.
If you could present your Authenticatable model and your reasoning and maybe migrations (?) I could advise you more. Right now I'm just pulling this out of thin air :-) But it seems like a good candidate for refactoring.
You can just move validation to before_save callback and it will work fine:
class User < ActiveRecord::Base
# Associations
belongs_to :authenticatable, polymorphic: true
# Validations
before_save :check_authenticatable
def check_authenticatable
unless authenticatable
errors[:customizable] << "can't be blank"
false
end
end
end
In the create action, I had to assign it manually:
#physician = Physician.new(params[:physician])
#physician.user.authenticatable = #physician
My problem is a little different (has_many and with different validation), but I think this should work.
I was able to get this to work by overriding the nested attribute setter.
class Physician
has_one :user, as: :authenticatable
accepts_nested_attributes_for :user
def user_attributes=(attribute_set)
super(attribute_set.merge(authenticatable: self))
end
end
To DRY it up, I moved the polymorphic code to a concern:
module Authenticatable
extend ActiveSupport::Concern
included do
has_one :user, as: :authenticatable
accepts_nested_attributes_for :user
def user_attributes=(attribute_set)
super(attribute_set.merge(authenticatable: self))
end
end
end
class Physician
include Authenticatable
...
end
For has_many associations, the same can be accomplished with a map:
class Physician
has_many :users, as: :authenticatable
accepts_nested_attributes_for :users
def users_attributes=(attribute_sets)
super(
attribute_sets.map do |attribute_set|
attribute_set.merge(authenticatable: self)
end
)
end
end
class User
belongs_to :authenticatable, polymorphic: true
validates :authenticatable, presence: true
end
All that said, I think konung's last comment is correct - your example does not look like a good candidate for polymorphism.
I'm not sure if this solves your problem, but I use something like this when validating that a polymorphic parent exists.
Here is some code that I used in a video model with the parent as the polymorphic association. This went in video.rb.
validates_presence_of :parent_id, :unless => Proc.new { |p|
# if it's a new record and parent is nil and addressable_type is set
# then try to find the parent object in the ObjectSpace
# if the parent object exists, then we're valid;
# if not, let validates_presence_of do it's thing
# Based on http://www.rebeccamiller-webster.com/2011/09/validate-polymorphic/
if (new_record? && !parent && parent_type)
parent = nil
ObjectSpace.each_object(parent_type.constantize) do |o|
parent = o if o.videos.include?(p) unless parent
end
end
parent
}