如何为after_save创建的对象返回验证错误?

时间:2012-06-08 17:31:37

标签: ruby-on-rails ruby ruby-on-rails-3.1 ice-cube

我正在编写一个使用ice_cube宝石来处理定期预订的预订系统。 Booking has_many BookingItem,每次出现在重复规则中,并且这些是在Booking的after_save回调调用的方法中创建的。

这一切都正常,直到我向BookingItem添加了验证,通过检查在给定时间内是否已经BookingItem来避免双重预订。此验证引发了一个错误,我希望在预订表单上显示该错误,但目前它只是默默地阻止Booking被保存 - 因为错误是由BookingItem提出的&# 39;没有被传回Booking的表格。

应用程序/模型/ booking.rb

class Booking < ActiveRecord::Base
  include IceCube

  has_many :booking_items, :dependent => :destroy

  after_save :recreate_booking_items!

  # snip

  private

  def recreate_booking_items!
    schedule.all_occurrences.each do |date|
      booking_items.create!(space: self.requested_space, 
                            booking_date: date.to_date,
                            start_time: Time.parse("#{date.to_date.to_default_s} #{self.start_time.strftime('%H:%M:00')}"),
                            end_time: Time.parse("#{date.to_date.to_default_s} #{self.end_time.strftime('%H:%M:00')}"))
    end
  end
end

应用程序/模型/ booking_item.rb

class BookingItem < ActiveRecord::Base
  belongs_to :booking

  validate :availability_of_space

  # snip

  private

    def availability_of_space
      unless space.available_between? DateTime.parse("#{booking_date}##{start_time}"), DateTime.parse("#{booking_date}##{end_time}")
        errors[:base] << "The selected space is not available between those times."
      end
    end
end

应用程序/视图/预订/ _form.html.erb

<% if @booking.errors.any? %>
  <div id="error_explanation">
    <p><%= pluralize(@booking.errors.count, "error") %> prohibited this booking from being saved:</p>
    <ul>
      <% @booking.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  </div>
<% end %>

<%= form_for(@booking, :html => { :class => "nice custom"}) do |f| %>
  ...
<% end %>

1 个答案:

答案 0 :(得分:2)

如果您使用after_save回调来创建BookingItem个对象,则您的选项会受到限制。

我会使用after_save而不是使用before_validation,而是进行一些调整以适应这种情况。

1)在BookingItem回调

中构建before_validation个对象
before_validation :recreate_booking_items!

def recreate_booking_items!
  schedule.all_occurrences.each do |date|
    booking_items.build(......
  end
end

请注意,我使用build代替create!

验证Booking对象时,BookingItem集合中的新booking_items对象也将得到验证。任何错误都将包含在主Booking对象的错误集合中,您可以像往常一样在视图中显示它们,因为Booking对象将无法保存。

注释

1)BookingItem对象在验证Booking对象时自动验证,因为它们是新记录并属于has_many关联。如果它们被持久化(即已经在数据库中),它们将不会被自动验证。

2)before_validation回调可以在对象的生命周期中多次调用,具体取决于您的代码。在这种情况下,每次调用回调时都会构建BookingItem个对象,这会导致重复。为防止这种情况,您可以在recreate_booking_items!的开头添加以下行:

booking_items.delete_all

当然,如果您在数据库中保留BookingItem个对象,则可能不希望这样做(见下文)。

3)此代码专门用于创建Booking个对象。如果您正在编辑已存在Booking个对象的现有BookingItem个对象,则可能需要进行某些修改,具体取决于您所需的功能。

更新:

在下面的评论中提及@ Simon的后续问题。

我可以想到你可能想要做的两种方式:

1)将验证保留在BookingItem中。

然后,我会在Booking中设置一个自定义验证器:

validate :validate_booking_items

def validate_booking_items
  booking_items.each do |bi|
    if bi.invalid?
      errors[:base] << "Booking item #{bi.<some property>} is invalid for <some reason>"
    end
  end
end

这为每个无效BookingBookingItem中添加了一条不错的自定义消息,但它也为每个BookingItem提供了自己的错误集合,您可以使用它来识别哪个booking_items无效。您可以像这样引用无效的booking_items

@ booking.booking_items.select {| bi | bi.errors.present?}

然后,如果您想在视图中显示无效的booking_items

f.fields_for :booking_items, f.object.booking_items.select {|bi| bi.errors.present? } do |bi|
end

这种方法的问题在于BookingItem可能由于多种原因而无效,并且尝试将所有这些原因添加到基本Booking错误集合中可能会变得混乱。

因此,另一种方法:

2)忘记Booking中的自定义验证器。依靠Rails&#39;自动验证has_many集合的非持久成员,以对每个BookingItem对象运行验证检查。这将为每个人提供一个错误集合。

然后,在您的视图中,您可以遍历无效的booking_items并显示其各自的错误。

<ul>
  <% @booking.booking_items.select {|bi| bi.errors.present? }.each do |bi| %>
    <li>
      Booking item <%= bi.name %> could not be saved because:
      <ul>
        <% bi.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul> 
    </li>
  <% end %>
</ul>

如果您使用此方法,您将拥有通用&#34;预订项目无效&#34;您的Booking对象错误集合中存在错误,因此您可能希望以某种方式忽略这些错误,以便它们不会显示。

注意:我不熟悉IceCube,但如果您通过BookingItem在表单中显示nested_attributes_for个对象,则可能会与构建BookingItem发生冲突before_validation回调中的对象。