2

我正在编写一个使用ice_cube gem 处理定期预订的预订系统。一个Bookinghas_many BookingItems,一个用于重复规则中的每次出现,这些是在一个由Booking的 after_save 回调调用的方法中创建的。

这一切都很好,直到我添加了一个验证,通过检查在给定的时间BookingItem还没有一个来避免重复预订。BookingItem此验证引发了一个错误,我想在预订表单上显示该错误,但目前它只是默默地阻止Booking保存 - 因为错误是由它引发的,BookingItem它没有被传递回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

app/views/booking/_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 %>
4

1 回答 1

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

Booking这为每个 invalid提供了一个很好的自定义消息BookingItem,但它也为每个提供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

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

因此,另一种方法:

2)忘记自定义验证器Booking。依靠 Rails 对集合的非持久成员的自动验证来运行每个对象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>

如果您使用这种方法,您的Booking对象错误集合中将出现一般的“预订项目无效”错误,因此您可能希望以某种方式忽略这些错误,以便它们不会显示。

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

于 2012-06-09T03:09:42.820 回答