Rails fields_for duplicates forms for existing records after validation

I came across a rather strange error. I have a nested form that works as expected, unless validation is performed on an existing record. When validation fails in an existing record, the re-created editable view contains fields for the invalid record twice. The first set of fields is filled in according to how the object is currently stored. The second set of fields is filled with information that was just submitted and declared invalid.

I have a basic nested form where the parent (ShiftPeriod) has_many children (Shifts) and each child belongs to the parent. ShiftPeriod accepts_nested_attributes for Shifts, with allow_destroy - true. I use nested_form stone, but I also tried using regular form_for with the same result

Form for ShiftPeriod (I deleted as much as possible to try to save it until I figure it out):

<%= nested_form_for @shift_period do |f| %>
  <%= f.fields_for :shifts %>
  <%= f.link_to_add "Add shift", :shifts %>
  <%= f.submit %>
<% end %>

Partial with fields for offsets:

<%= 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" %>

The corresponding part of the Shift object is:

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

The corresponding part of the ShiftPeriod object is:

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

Controller: as_many children (Shifts), and each child belongs to the parent. ShiftPeriod accepts_nested_attributes for Shifts, with allow_destroy - true. I use nested_form stone, but I also tried using regular form_for with the same result

Controller:

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

Form for ShiftPeriod (I deleted as much as possible to try to save it until I figure it out):

<%= nested_form_for @shift_period do |f| %>
  <%= f.fields_for :shifts %>
  <%= f.link_to_add "Add shift", :shifts %>
  <%= f.submit %>
<% end %>

Partial with fields for offsets:

<%= 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" %>

The corresponding part of the Shift object is:

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

The corresponding part of the ShiftPeriod object is:

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

Controller:

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
+3
1

, , , .

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

+1

All Articles