Champs de rails_pour les formulaires en double pour les enregistrements existants après validation
-
28-10-2019 - |
Question
J'ai rencontré une erreur plutôt bizarre. J'ai un formulaire imbriqué qui fonctionne comme prévu, sauf lorsqu'une validation échoue sur un enregistrement existant. Lorsqu'une validation échoue sur un enregistrement existant, la vue d'édition re-rendue contient deux fois les champs de l'enregistrement non valide. Le premier ensemble de champs est rempli en fonction de la façon dont l'objet est actuellement stocké. Le deuxième ensemble de champs est rempli avec les informations qui viennent d'être soumises et qui ont été jugées invalides.
J'ai une forme imbriquée de base où un parent (ShiftPeriod) a_many enfants (Shifts) et chaque enfant appartient au parent. ShiftPeriod accepte_nested_attributes pour Shifts, avec allow_destroy défini sur true. J'utilise le gem nested_form, mais j'ai aussi essayé d'utiliser un form_for régulier avec le même résultat
Le formulaire pour ShiftPeriod (j'ai supprimé autant que possible pour essayer de le garder simple jusqu'à ce que je comprenne cela):
<%= nested_form_for @shift_period do |f| %>
<%= f.fields_for :shifts %>
<%= f.link_to_add "Add shift", :shifts %>
<%= f.submit %>
<% end %>
Le partiel avec les champs pour les équipes:
<%= f.select :member_id, options_for_select(Member.crew_members.order('last_name').collect{|member| ["#{member.last_name}, #{member.first_name}", member.id]}, :selected => Member.where(:bars_num == 1).first.id) %>
<%= f.collection_select :start_time, @time_range, :dup, :hour, :selected => Time.parse(f.object.start_time.to_s) || @shift_period.start_time %>
<%= f.collection_select :end_time, @time_range, :dup, :hour, :selected => f.object.new_record? ? @time_range.last : Time.parse(f.object.end_time.to_s) %>
<%= f.select :repeat_month, options_for_select([['Never', false], ['Monthly', true]]) %>
<%= f.select :repeat, options_for_select([['Never', 0], ['Every Other Week', 1], ['Every Week', 2]]) %>
<%= f.link_to_remove "Remove" %>
La partie pertinente de l'objet Shift:
class Shift < ActiveRecord::Base
include Coverage::SetOperations
belongs_to :member
belongs_to :shift_period
delegate :date, :to => :shift_period
delegate :daynight, :to => :shift_period
after_save :update_shift_period_open_slots
after_destroy :update_shift_period_open_slots
validates_presence_of :member, :start_time, :end_time, :shift_period
La partie pertinente de l'objet ShiftPeriod:
class ShiftPeriod < ActiveRecord::Base
has_many :shifts
has_many :open_slots, :dependent => :destroy
has_many :calls
after_create :update_open_slots
validates_presence_of :date
validates :date, :uniqueness => {:scope => :daynight}
accepts_nested_attributes_for :shifts, :reject_if => lambda {|a| a[:start_time].blank? || a[:end_time].blank? || a[:member_id].blank? || a[:repeat].blank? }, :allow_destroy => true
Le contrôleur: as_many enfants (Shifts) et chaque enfant appartient au parent. ShiftPeriod accepte_nested_attributes pour Shifts, avec allow_destroy défini sur true. J'utilise le gem nested_form, mais j'ai aussi essayé d'utiliser un form_for régulier avec le même résultat
Le contrôleur:
def edit
@shift_period = ShiftPeriod.find(params[:id])
set_time_range
end
def set_time_range
@time_range = @shift_period.daynight ? (6..18).to_a : (18..23).to_a + (0..6).to_a
@time_range.collect!{|val| @shift_period.start_time - @shift_period.start_time.hour.hours + val.hours }
end
def update
@shift_period = ShiftPeriod.find(params[:id])
respond_to do |format|
if @shift_period.update_attributes(params[:shift_period])
format.html { redirect_to(schedule_path(:date => @shift_period.date, :notice => 'Shift period was successfully updated')) }
format.xml { head :ok }
else
set_time_range
format.html { render :action => "edit" }
format.xml { render :xml => @shift_period.errors, :status => :unprocessable_entity }
end
end
end
Le formulaire pour ShiftPeriod (j'ai supprimé autant que possible pour essayer de le garder simple jusqu'à ce que je comprenne cela):
<%= nested_form_for @shift_period do |f| %>
<%= f.fields_for :shifts %>
<%= f.link_to_add "Add shift", :shifts %>
<%= f.submit %>
<% end %>
Le partiel avec les champs pour les équipes:
<%= f.select :member_id, options_for_select(Member.crew_members.order('last_name').collect{|member| ["#{member.last_name}, #{member.first_name}", member.id]}, :selected => Member.where(:bars_num == 1).first.id) %>
<%= f.collection_select :start_time, @time_range, :dup, :hour, :selected => Time.parse(f.object.start_time.to_s) || @shift_period.start_time %>
<%= f.collection_select :end_time, @time_range, :dup, :hour, :selected => f.object.new_record? ? @time_range.last : Time.parse(f.object.end_time.to_s) %>
<%= f.select :repeat_month, options_for_select([['Never', false], ['Monthly', true]]) %>
<%= f.select :repeat, options_for_select([['Never', 0], ['Every Other Week', 1], ['Every Week', 2]]) %>
<%= f.link_to_remove "Remove" %>
La partie pertinente de l'objet Shift:
class Shift < ActiveRecord::Base
include Coverage::SetOperations
belongs_to :member
belongs_to :shift_period
delegate :date, :to => :shift_period
delegate :daynight, :to => :shift_period
after_save :update_shift_period_open_slots
after_destroy :update_shift_period_open_slots
validates_presence_of :member, :start_time, :end_time, :shift_period
La partie pertinente de l'objet ShiftPeriod:
class ShiftPeriod < ActiveRecord::Base
has_many :shifts
has_many :open_slots, :dependent => :destroy
has_many :calls
after_create :update_open_slots
validates_presence_of :date
validates :date, :uniqueness => {:scope => :daynight}
accepts_nested_attributes_for :shifts, :reject_if => lambda {|a| a[:start_time].blank? || a[:end_time].blank? || a[:member_id].blank? || a[:repeat].blank? }, :allow_destroy => true
Le contrôleur:
def edit
@shift_period = ShiftPeriod.find(params[:id])
set_time_range
end
def set_time_range
@time_range = @shift_period.daynight ? (6..18).to_a : (18..23).to_a + (0..6).to_a
@time_range.collect!{|val| @shift_period.start_time - @shift_period.start_time.hour.hours + val.hours }
end
def update
@shift_period = ShiftPeriod.find(params[:id])
respond_to do |format|
if @shift_period.update_attributes(params[:shift_period])
format.html { redirect_to(schedule_path(:date => @shift_period.date, :notice => 'Shift period was successfully updated')) }
format.xml { head :ok }
else
set_time_range
format.html { render :action => "edit" }
format.xml { render :xml => @shift_period.errors, :status => :unprocessable_entity }
end
end
end
La solution
Voici le hack que j'utilise pour l'instant pour contourner le problème, de meilleures idées seraient grandement appréciées.
def update
@shift_period = ShiftPeriod.find(params[:id])
if @shift_period.update_attributes(params[:shift_period])
redirect_to(schedule_path(:date => @shift_period.date, :notice => 'Shift period was successfully updated'))
else
set_time_range
new_records = []
@shift_period.shifts.each{|shift| if shift.new_record? then new_records << shift end}
@shift_period.shifts.slice!(0,@shift_period.shifts.length/2)
@shift_period.shifts += new_records
render :action => "edit"
end
fin