Since Mario commented on my question and asked if I solved it, I thought I would share my solution.
I should say that I'm sure this isn't a very elegant solution, and it's not great code. But it's what I came up with, and it works. Since this question is pretty technical, I'm not posting pseudo-code here - I'm posting the full code for both the Checklist model and the Checklists controller update action (the parts of the code that apply to this question, anyway). I'm also pretty sure my transaction blocks aren't actually doing anything (I need to fix those).
The basic idea is I broke out the update action manually. Rather than relying on update_attributes (and accepts_nested_attributes_for), I manually update the checklist in two phases:
- Did the actual checklist object change (a checklist only has a name and description)? If it did, create a new checklist, make the new one a child of the old one, and set the new one up with whatever jobs were added or selected for it.
- If the checklist itself didn't change (name and description stayed the same), did the jobs assigned to it change? If they did, archive job assignments that were removed, and add any new job assignments.
There's some "submission" stuff that I think is safe to ignore here (it's basically logic to determine if it even matters how the checklist changed - if there aren't any submissions (records of a checklist's historical data) then just update the checklist in place without doing any of this archiving or adding/subtracting jobs stuff).
I don't know if this will be helpful, but here it is anyway.
Code - checklist.rb (model)
class Checklist < ActiveRecord::Base
scope :archived_state, lambda {|s| where(:archived => s) }
belongs_to :creator, :class_name => "User", :foreign_key => "creator_id"
has_many :submissions
has_many :checklists_jobs, :dependent => :destroy, :order => 'checklists_jobs.job_position'#, :conditions => {'archived_at' => nil}
has_many :jobs, :through => :checklists_jobs
has_many :unarchived_jobs, :through => :checklists_jobs,
:source => :job,
:conditions => ['checklists_jobs.archived = ?', false], :order => 'checklists_jobs.job_position'
has_many :checklists_workdays, :dependent => :destroy
has_many :workdays, :through => :checklists_workdays
def make_child_of(old_checklist)
self.parent_id = (old_checklist.parent_id == 0) ? old_checklist.id : old_checklist.parent_id
self.predecessor_id = old_checklist.id
self.version = (old_checklist.version + 1)
end
def set_new_jobs(new_jobs)
new_jobs.to_a.each do |job|
self.unarchived_jobs << Job.find(job) unless job.nil?
end
end
def set_jobs_attributes(jobs_attributes, old_checklist)
jobs_attributes.each do |key, entry|
# Job already exists and should have a CJ
if entry[:id] && !(entry[:_destroy] == '1')
old_cj = old_checklist.checklists_jobs.archived_state(:false).find_by_job_id(entry[:id])
new_cj = ChecklistsJob.new job_position: old_cj.job_position, job_required: old_cj.job_required
new_cj.checklist = self
new_cj.job = old_cj.job
new_cj.save!
# New job, should be created and added to new checklist only
else
unless entry[:_destroy] == '1'
entry.delete :_destroy
self.jobs << Job.new(entry)
end
end
end
end
def set_checklists_workdays!(old_checklist)
old_checklist.checklists_workdays.archived_state(:false).each do |old_cw|
new_cw = ChecklistsWorkday.new checklist_position: old_cw.checklist_position
new_cw.checklist = self
new_cw.workday = old_cw.workday
new_cw.save!
old_cw.archive
old_cw.save!
end
end
def update_checklists_jobs!(jobs_attributes)
jobs_attributes.each do |key, entry|
if entry[:id] # Job was on self when #edit was called
old_cj = self.checklists_jobs.archived_state(:false).find_by_job_id(entry[:id])
#puts "OLD!! "+old_cj.id.to_s
unless entry[:_destroy] == '1'
new_cj = ChecklistsJob.new job_position: old_cj.job_position, job_required: old_cj.job_required
new_cj.checklist = self
new_cj.job = old_cj.job
new_cj.save!
end
old_cj.archive
old_cj.save!
else # Job was created on this checklist
unless entry[:_destroy] == '1'
entry.delete :_destroy
self.jobs << Job.new(entry)
end
end
end
end
end
Code - checklists_controller.rb (controller)
class ChecklistsController < ApplicationController
before_filter :admin_user
def update
@checklist = Checklist.find(params[:id])
@testChecklist = Checklist.find(params[:id])
@oldChecklist = Checklist.find(params[:id])
@job_list = @checklist.unarchived_jobs.exists? ? Job.archived_state(:false).where( 'id not in (?)', @checklist.unarchived_jobs) : Job.archived_state(:false)
checklist_ok = false
# If the job is on a submission, do archiving/copying; else just update it
if @checklist.submissions.count > 0
puts "HERE A"
# This block will tell me if I need to make new copies or not
@testChecklist.attributes=(params[:checklist])
jobs_attributes = params[:checklist][:jobs_attributes]
if @testChecklist.changed?
puts "HERE 1"
params[:checklist].delete :jobs_attributes
@newChecklist = Checklist.new(params[:checklist])
@newChecklist.creator = current_user
@newChecklist.make_child_of(@oldChecklist)
@newChecklist.set_new_jobs(params[:new_jobs])
begin
ActiveRecord::Base.transaction do
@newChecklist.set_jobs_attributes(jobs_attributes, @oldChecklist) if jobs_attributes
@newChecklist.set_checklists_workdays!(@oldChecklist)
@newChecklist.save!
@oldChecklist.archive
@oldChecklist.save!
@checklist = @newChecklist
checklist_ok = true
end
rescue ActiveRecord::RecordInvalid
# This is a NEW checklist, so it's acting like it's "new" - WRONG?
puts "RESCUE 1"
@checklist = @newChecklist
@jobs = @newChecklist.jobs
checklist_ok = false
end
elsif @testChecklist.changed_for_autosave? || params.has_key?(:new_jobs)
puts "HERE 2"
# Associated Jobs have changed, so archive old checklists_jobs,
# then set checklists_jobs based on params[:checklist][:jobs_attributes] and [:new_jobs]
@checklist.set_new_jobs(params[:new_jobs])
begin
ActiveRecord::Base.transaction do
@checklist.update_checklists_jobs!(jobs_attributes) if jobs_attributes
@checklist.save!
checklist_ok = true
end
rescue ActiveRecord::RecordInvalid
puts "RESCUE 2"
@jobs = @checklist.unarchived_jobs
checklist_ok = false
end
else
checklist_ok = true # There were no changes to the Checklist or Jobs
end
else
puts "HERE B"
@checklist.set_new_jobs(params[:new_jobs])
begin
ActiveRecord::Base.transaction do
@checklist.update_attributes(params[:checklist])
checklist_ok = true
end
rescue ActiveRecord::RecordInvalid
puts "RESCUE B"
@jobs = @checklist.jobs
checklist_ok = false
end
end
respond_to do |format|
if checklist_ok
format.html { redirect_to @checklist, notice: 'List successfully updated.' }
format.json { head :no_content }
else
flash.now[:error] = 'There was a problem updating the List.'
format.html { render action: "edit" }
format.json { render json: @checklist.errors, status: :unprocessable_entity }
end
end
end
end
Code - Checklist form
<%= form_for @checklist, :html => { :class => 'form-inline' } do |f| %>
<div>
<%= f.text_area :name, :rows => 1, :placeholder => 'Name the list...', :class => 'autoresizer checklist-name' %></br>
<%= f.text_area :description, :rows => 1, :placeholder => 'Optional description...', :class => 'autoresizer' %>
</div>
<%= f.fields_for :jobs, :html => { :class => 'form-inline' } do |j| %>
<%= render "job_fields", :j => j %>
<% end %>
<span class="add-new-job-link"><%= link_to_add_fields "add a new job", f, :jobs %></span>
<div class="form-actions">
<%= f.submit nil, :class => 'btn btn-primary' %>
<%= link_to 'Cancel', checklists_path, :class => 'btn' %>
</div>
<% unless @job_list.empty? %>
<legend>Add jobs from the Job Bank</legend>
<% @job_list.each do |job| %>
<div class="toggle">
<label class="checkbox text-justify" for="<%=dom_id(job)%>">
<%= check_box_tag "new_jobs[]", job.id, false, id: dom_id(job) %><strong><%= job.name %></strong> <small><%= job.description %></small>
</label>
</div>
<% end %>
<div class="form-actions">
<%= f.submit nil, :class => 'btn btn-primary' %>
<%= link_to 'Cancel', checklists_path, :class => 'btn' %>
</div>
<% end %>
<% end %>