我的问题是我遇到了accepts_nested_attributes_for的限制,所以我需要弄清楚如何在我自己复制该功能,以便有更大的灵活性。所以我的问题是:如果我想模仿和增加accepts_nested_attributes_for,我的表单,控制器和模型应该是什么样子?真正的技巧是我需要能够使用现有的关联/属性更新现有的和新的模型。
我正在构建一个使用嵌套表单的应用程序。我最初使用这个RailsCast作为蓝图(利用accepts_nested_attributes_for): RailsCast 196:嵌套模型表单。
我的应用程序是与工作(任务)的清单,我让用户更新清单(名称,描述)和添加/删除相关的工作在一个单一的形式。这工作得很好,但我遇到了问题,当我把它纳入我的应用程序的另一个方面:历史通过版本控制。
我的应用程序的很大一部分是我需要记录我的模型和关联的历史信息。我最终完成了自己的版本控制(这里是我的问题,我在这里描述了我的决策过程/考虑因素),其中很大一部分是一个工作流,我需要为旧的东西创建一个新版本,对新版本进行更新,存档旧版本。这对用户来说是不可见的,他们认为这种体验只是通过UI更新一个模型。
Code - models
#checklist.rb
class Checklist < ActiveRecord::Base
has_many :jobs, :through => :checklists_jobs
accepts_nested_attributes_for :jobs, :reject_if => lambda { |a| a[:name].blank? }, :allow_destroy => true
end
#job.rb
class Job < ActiveRecord::Base
has_many :checklists, :through => :checklists_jobs
end
Code - current form(注:@jobs被定义为未存档的作业,在这个清单控制器编辑动作中;@checklist)
<%= simple_form_for @checklist, :html => { :class => 'form-inline' } do |f| %>
<fieldset>
<legend><%= controller.action_name.capitalize %> Checklist</legend><br>
<%= f.input :name, :input_html => { :rows => 1 }, :placeholder => 'Name the Checklist...', :class => 'autoresizer' %>
<%= f.input :description, :input_html => { :rows => 3 }, :placeholder => 'Optional description...', :class => 'autoresizer' %>
<legend>Jobs on this Checklist - [Name] [Description]</legend>
<%= f.fields_for :jobs, @jobs, :html => { :class => 'form-inline' } do |j| %>
<%= render "job_fields_disabled", :j => j %>
<% end %>
</br>
<p><%= link_to_add_fields "+", f, :jobs %></p>
<div class="form-actions">
<%= f.submit nil, :class => 'btn btn-primary' %>
<%= link_to 'Cancel', checklists_path, :class => 'btn' %>
</div>
</fieldset>
<% end %>
checklists_controller.rb#Update
def update
@oldChecklist = Checklist.find(params[:id])
# Do some checks to determine if we need to do the new copy/archive stuff
@newChecklist = @oldChecklist.dup
@newChecklist.parent_id = (@oldChecklist.parent_id == 0) ? @oldChecklist.id : @oldChecklist.parent_id
@newChecklist.predecessor_id = @oldChecklist.id
@newChecklist.version = (@oldChecklist.version + 1)
@newChecklist.save
# Now I've got a new checklist that looks like the old one (with some updated versioning info).
# For the jobs associated with the old checklist, do some similar archiving and creating new versions IN THE JOIN TABLE
@oldChecklist.checklists_jobs.archived_state(:false).each do |u|
x = u.dup
x.checklist_id = @newChecklist.id
x.save
u.archive
u.save
end
# Now the new checklist's join table entries look like the old checklist's entries did
# BEFORE the form was submitted; but I want to update the NEW Checklist so it reflects
# the updates made in the form that was submitted.
# Part of the params[:checklist] has is "jobs_attributes", which is handled by
# accepts_nested_attributes_for. The problem is I can't really manipulate that hash very
# well, and I can't do a direct update with those attributes on my NEW model (as I'm
# trying in the next line) due to a built-in limitation.
@newChecklist.update_attributes(params[:checklist])
这就是我遇到accepts_nested_attributes_for限制的地方(这里有很好的文档说明)。我得到了"无法找到ID=X的Model1与ID=Y的Model2"异常,这基本上是按设计的。
所以,我怎么能创建多个嵌套模型和添加/删除他们的父模型的形式类似于什么accepts_nested_attributes_for做,但在我自己?
我看过的选项是最好的吗?真正的技巧是我需要能够更新现有的和新的模型与现有的关联/属性。我不能链接它们,所以我就命名它们。
Redtape(在github上)Virtus(也叫github)谢谢你的帮助!
您可能想要删除复杂的accepts_nested内容,并创建一个自定义类或模块来包含所需的所有步骤。
这篇文章有一些有用的东西
http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/特别是第3点
既然Mario评论了我的问题并问我是否解决了这个问题,我想我应该分享一下我的解决方案。
我应该说,我确信这不是一个非常优雅的解决方案,它不是一个伟大的代码。但这就是我想出来的,而且很有效。由于这个问题是相当技术性的,我不在这里发布伪代码-我发布了清单模型和Checklists控制器更新动作的完整代码(无论如何,适用于这个问题的代码部分)。我也很确定我的事务块实际上没有做任何事情(我需要修复这些)。
基本思想是我手动执行更新操作。我没有依赖于update_attributes(和accepts_nested_attributes_for),而是分两个阶段手动更新检查表:
- 实际的检查表对象是否发生变化(检查表只有名称和描述)?如果是,创建一个新的检查表,使新检查表成为旧检查表的子检查表,并为新检查表添加或选择任何作业。
- 如果检查表本身没有改变(名称和描述保持不变),分配给它的工作是否改变了?如果没有,则存档已删除的作业分配,并添加任何新的作业分配。
有一些"提交"的东西,我认为在这里可以安全地忽略(基本上是逻辑来确定是否重要的清单如何更改-如果没有任何提交(清单的历史数据记录),那么只需在适当的地方更新清单,而不做任何归档或添加/减去作业的东西)。
我不知道这是否有帮助,但无论如何,这里是。
代码-检查表。rb(模型)
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(控制器)
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
代码-检查表
<%= 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 %>