nested forms - how to use before/after destroy callback ?

84 views
Skip to first unread message

Javix

unread,
Mar 14, 2013, 3:12:37 PM3/14/13
to rubyonra...@googlegroups.com
How is it possible to use a callback and which one to avoid the destroying of the last association.
For example, there are 3 following models:

#timesheet.rb
class Timesheet < ActiveRecord::Base
  has_many :activities, dependent: :destroy
  has_many :time_entries, through: :activities
  accepts_nested_attributes_for :activities, allow_destroy: true
end

#activity.rb
class Activity < ActiveRecord::Base
  has_many :time_entries, order: :workdate, dependent: :destroy
  accepts_nested_attributes_for :time_entries, allow_destroy: true, reject_if: proc { |a| a[:worktime].blank? }
end

#time_entry.rb
class TimeEntry < ActiveRecord::Base
  belongs_to :activity
  validates :worktime, presence: true, inclusion: { in: [0.5, 1] } 
end

Every timesheet is created for 7 days (tile_entries) for one activity in the the same form. To delete I use the technic with jQuery explained at Railscats:
#add_remove_fields.js

function remove_fields(link) {
  $(link).prev("input[type=hidden]").val("1");
  $(link).closest(".fields").hide();
}

function add_fields(link, association, content) {
  var new_id = new Date().getTime();
  var regexp = new RegExp("new_" + association, "g");
  $(link).parent().before(content.replace(regexp, new_id));
}

#application_helper.rb

def link_to_remove_fields(name, f)
    f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)")
  end

  def link_to_add_fields(name, f, association, timesheet)
...
end

I tried several ways with after_destroy hook, - it didn't work.
Any idea ? Thanks and regards.

Javix

unread,
Mar 19, 2013, 10:30:24 AM3/19/13
to rubyonra...@googlegroups.com
So still no idea how to avoid the destroy of the last association. I do all the processing in the 'update ' action of TimesheetsController. I tried with 'after_update' hook in the Timsheet model as follows:


#timesheet.rb
class Timesheet < ActiveRecord::Base
  accepts_nested_attributes_for :activities, allow_destroy: true
  after_update :check_an_activity_present

  private
    def check_an_activity_present     
      raise "You should have at least ONE activity present" if activities.empty?
    end
end

An I put the update_attributes in the begin/rescue block in the controller:
#timesheets_controller.rb
class TimesheetsController < ApplicationController
def update
    begin
      @timesheet = current_user.timesheets.find(params[:id])         
      if @timesheet.update_attributes(params[:timesheet])
        flash[:success] = 'Timesheet updated sucessfully'
        redirect_to @timesheet
      else
        load_entries
        render 'edit'
      end
    rescue Exception => e
      flash.now[:error] = e.message
      @entries = @timesheet.time_entries
      render 'edit'
    end
   
private
  def load_entries
      @entries = @timesheet.build_and_sort_time_entries
    end   
  end
end

The problem is that the activity with corresponding time entries collection is not destroyed even if I added a new activity with corresponding time entries. So after the update executed, I have 2 activities saved with corresponding time entries instad of having just the ONLY one, entered just after the destroying the previous one. Any idea? Thank you.

Javix

unread,
Mar 19, 2013, 4:51:19 PM3/19/13
to rubyonra...@googlegroups.com
Finally, after_update callback works as needed, te problem was in the new fields_for generation. The below is the correct and updated version:


#timesheet.rb
class Timesheet < ActiveRecord::Base
  has_many :activities, dependent: :destroy
  has_many :time_entries, through: :activities
  accepts_nested_attributes_for :activities, allow_destroy: true
end

#activity.rb
class Activity < ActiveRecord::Base
  has_many :time_entries, order: :workdate, dependent: :destroy
  accepts_nested_attributes_for :time_entries, allow_destroy: true, reject_if: proc { |a| a[:worktime].blank? }
    validate :one_time_entry_present
private
    def one_time_entry_present
      errors[:base] << "At least one entry should be entered" if time_entries.empty?
    end 
end

#time_entry.rb
class TimeEntry < ActiveRecord::Base
  belongs_to :activity
  validates :worktime, presence: true, inclusion: { in: [0.5, 1] } 
end

Every timesheet is created for 7 days (tile_entries) for one activity in the the same form. To delete I use the technic with jQuery explained at Railscats:
#add_remove_fields.js

function remove_fields(link) {
  $(link).prev("input[type=hidden]").val("1");
  $(link).closest('table').find("tr.entry_header").hide();
  $(link).closest(".fields").hide();
}

function add_fields(link, association_id, content) {
  var new_id = new Date().getTime();
  var regexp = new RegExp(association_id, "g");
  $(link).parent().before(content.replace(regexp, new_id));
}

#application_helper.rb

def link_to_remove_fields(name, f)
    f.hidden_field(:_destroy) + link_to_function(name, "remove_fields(this)")
  end

 def link_to_add_fields(name, f, association, timesheet)
    new_object = f.object.send(association).klass.new
    id = new_object.object_id    
    timesheet.create_activity_days(new_object)
    
    fields = f.fields_for(association, new_object, child_index: id) do |builder|
      render(association.to_s.singularize + "_fields", f: builder)
    end    
                    

    link_to_function(name, "add_fields(this, \"#{id}\", \"#{escape_javascript(fields)}\")")
  end

#timesheets_controller.rb

def update
    begin
      @timesheet = current_user.timesheets.find(params[:id])          
      if @timesheet.update_attributes(params[:timesheet])
        flash[:success] = 'Timesheet updated sucessfully'
        redirect_to @timesheet
      else
        load_entries
        render 'edit'
      end
    rescue Exception => e
      flash[:error] = e.message
      redirect_to edit_timesheet_path(@timesheet)
    end
    
  end

Hope this helps. 
Reply all
Reply to author
Forward
0 new messages