我正在编写一个预订系统,该系统使用 ice_cube gem 处理定期预订。一个 Booking
has_many BookingItem
s,重复规则中每个出现一个,这些是在由 Booking
的 after_save 回调调用的方法中创建的。
这一切都很好,直到我向BookingItem
添加验证,通过检查给定时间是否还没有BookingItem
来避免重复预订。此验证引发了一个错误,我想在预订表单上显示该错误,但目前它只是静默地阻止保存Booking
- 因为错误是由BookingItem
引发的,因此它没有传递回Booking
的表单。
app/models/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
app/models/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 %>
如果使用after_save
回调来创建BookingItem
对象,则选项会受到一定限制。
与其使用 after_save
,我会使用 before_validation
并进行一些调整以适应这种情况。
1) 在before_validation
回调中构建BookingItem
对象
before_validation :recreate_booking_items!
和
def recreate_booking_items!
schedule.all_occurrences.each do |date|
booking_items.build(......
end
end
请注意,我使用的是build
而不是create!
验证Booking
对象时,还将验证booking_items
集合中的新BookingItem
对象。 任何错误都将包含在主Booking
对象的错误集合中,您可以像往常一样在视图中显示它们,因为Booking
对象将无法保存。
笔记
1) 验证Booking
对象时,将自动验证BookingItem
对象,因为它们是新记录并且属于has_many
关联。 如果它们被持久化(即已经在数据库中),则不会自动验证它们。
2) before_validation
回调可以在对象的生命周期中多次调用,具体取决于您的代码。 在这种情况下,每次调用回调时都会构建BookingItem
对象,这将导致重复。为了防止这种情况,您可以在recreate_booking_items!
的开头添加以下行:
booking_items.delete_all
当然,如果您已在数据库中保留了BookingItem
对象,则可能不希望这样做(见下文)。
3) 此代码专为创建Booking
对象而设计。如果要编辑已保留BookingItem
对象的现有Booking
对象,则可能需要进行某些修改,具体取决于所需的功能。
更新:
在下面的评论中解决@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
这会为每个无效BookingItem
Booking
一个很好的自定义消息,但它也为每个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 对 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,但如果您通过 nested_attributes_for
在表单中显示BookingItem
对象,这可能会与在before_validation
回调中构建BookingItem
对象发生冲突。