I am having an issue understanding how to use Rails' delegated types when it comes to validations failing on the delegatee.
Having the following code
inbox.rb
class Inbox < ApplicationRecord
delegate :name, to: :inboxable
delegated_type :inboxable, types: %w[ Mailbox Messagebox ], dependent: :destroy
end
class Mailbox < ApplicationRecord
include Inboxable
belongs_to :inbox_domain
validates :alias, presence: true, uniqueness: true
def name
"#{self.alias}@#{self.domain.name}"
end
end
messagees_controller.rb
def create
@mailbox = Inbox.create inboxable: Mailbox.new(mailbox_params)
if @mailbox.save
redirect_to @mailbox.inboxable, notice: "<b>#{@mailbox.name}</b> was created."
else
render :new
end
end
private
def mailbox_params
params.require(:mailbox).permit(:alias, :inbox_domain_id)
end
When i want to create a mailbox where the alias is already taken, the following error is thrown because Mailbox.new fails the validation
ActiveRecord::NotNullViolation (PG::NotNullViolation: ERROR: null value in column "inboxable_id" violates not-null constraint
DETAIL: Failing row contains (13, 2021-09-26 20:48:53.970799, 2021-09-26 20:48:53.970799, Mailbox, null, f).
):
Tried solution
What is the correct way to handle this scenario? I have tried to check explicitly Mailbox.new first, like this:
mailbox = Mailbox.new(mailbox_params)
if mailbox.valid?
@inbox = Inbox.create inboxable: mailbox
......
While it technically works, it's a mess once you also have to validate attributes on Inbox itself
CodePudding user response:
Use validates_associated
to trigger the validations on the associated record:
class Inbox < ApplicationRecord
delegate :name, to: :inboxable
delegated_type :inboxable, types: %w[ Mailbox Messagebox ], dependent: :destroy
validates_associated :inboxable
end
This will add an error ("Inboxable is invalid") to the errors object on this model and prevent saving if the associated mailbox is not valid.
What you want in your controller is:
def create
# .create both instanciates and saves the record - not what you want here
@mailbox = Inbox.new(inboxable: Mailbox.new(mailbox_params))
if @mailbox.save
redirect_to @mailbox.inboxable, notice: "<b>#{@mailbox.name}</b> was created."
else
render :new
end
end
If you want to display the errors for the associated item you need to access and loop through the errors object on it:
# app/views/shared/_errors.html.erb
<ul>
<% object.errors.each do |attribute, message| %>
<li><%= message %>
<% end %>
</ul>
<%= form_with(model: @inbox) do |form| %>
<% if form.object.invalid? %>
<%= render(partial: 'shared/errors', object: form.object) %>
<% if form.object.inboxable.invalid? %>
<%= render(partial: 'shared/errors', object: form.object.inboxable) %>
<% end %>
<% end %>
# ...
<% end %>