5

我的问题是我遇到了accep_nested_attributes_for的局限性,所以我需要弄清楚如何自己复制这个功能以获得更大的灵活性。 (请参阅下面的内容,确切地说是什么让我心烦意乱。)所以我的问题是:如果我想要模仿并增强accept_nested_attributes_for,我的表单,控制器和模型应该是什么样子?真正的技巧是我需要能够使用现有的关联/属性更新现有的和新的模型。Rails - 如何管理嵌套属性而不使用accepts_nested_attributes_for?

我正在构建一个使用嵌套窗体的应用程序。我最初使用这个RailsCast作为蓝图(利用accept_nested_attributes_for):Railscast 196: Nested Model Form

我的应用程序是清单与作业(任务),我让用户更新清单(名称,说明)和添加/删除关联的作业在一个单一的形式。这很好,但是当我将这个应用到我的应用程序的另一个方面时,我遇到了问题:通过版本控制的历史记录。

我的应用很大一部分是我需要记录我的模型和关联的历史信息。我结束了自己的版本(here是我的问题,我描述了我的决策过程/注意事项),其中很大一部分是我需要创建旧版本的新版本,对新版本进行更新的工作流程,存档旧版本。这对于用户来说是不可见的,他们将体验视为仅仅通过UI更新模型。

码 - 模型

#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 

码 - 目前的形式(注:@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#代码片段#更新

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]) 

这就是我运行i通过accept_nested_attributes_for的限制(它被记录得很好here。我得到了“ID = Y的Model2 ID = X的Model1无法找到”异常,这基本上与设计一致。

那么,我该如何创建多个嵌套模型,并在父模型的表单上添加/删除它们,类似于accept_nested_attributes_for的做法,但是我自己呢?

我见过的选项 - 是最好的选择之一吗? 真正的诀窍是我需要能够使用现有的关联/属性来更新现有模型和新模型。我无法链接它们,所以我只是将它们命名。

Redtape(在github) 了Virtus(也github上)

感谢您的帮助!

+0

如果你解决了这个问题,我会非常感兴趣的看到你的解决方案。 – 2013-07-02 02:39:28

+0

马里奥,我确实解决了它,并且我在下面发布了我的代码。这不是很好的代码,但是如果你在类似的事情上挣扎,也许会给你一些想法。任何问题,只是在这里或我的答案评论,我会尽力澄清,如果我可以。 – JoshDoody 2013-07-04 06:43:11

回答

1

由于马里奥评论我的问题,问我解决了这个问题,我想我会分享我的解决方案。

我应该说我确定这不是一个非常优雅的解决方案,它不是很棒的代码。但这是我想出来的,而且很有效。由于这个问题非常具有技术性,因此我不在这里发布伪代码 - 我发布了Checklist模型和Checklists控制器更新操作(适用于此问题的代码部分)的完整代码。我也很确定我的交易区块实际上没有做任何事情(我需要修复这些)。

基本思路是我手动分解了更新操作。而不是依靠update_attributes方法(和accepts_nested_attributes_for),我手动分两期更新清单:

  1. 做了实际的清单对象的变化(清单只有一个名称和说明)?如果是的话,创建一个新的清单,使新的清单成为旧清单的一个孩子,并为新清单添加或选择任何作业。
  2. 如果清单本身没有改变(名称和描述保持不变),分配给它的作业是否改变?如果他们这样做,将归档的作业分配归档并添加新的作业分配。

有一些“提交”的东西,我认为是安全的,这里忽略(这是基本的逻辑,以确定它甚至事项清单是如何改变 - 如果没有任何意见(检查表的历史数据记录)那么只需更新检查表而不做任何这种归档或加/减工作的东西)。

我不知道这是否会有所帮助,但无论如何它在这里。

码 - checklist.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 

码 - 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 %> 
+0

IMO,你应该避免直接操纵'_destroy'参数。我认为这是一个流血的实现细节,所以客户可以通过javascript来完成工作。在服务器上,使用'mark_for_destruction'和'marked_for_destruction?'参见http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html – 2015-04-10 17:11:33

5

您可能会想要复制一个复杂的acceptenesnes内容并创建一个自定义类或模块来包含所需的所有步骤。

有一些有用的东西,在这个岗位

http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

特别是3点

+0

我认为你是对的。我之前看过那篇博文,而且我认为它实际上激发了我上面提到的redtape gem。我一直在别处问,redtape已经出现了几次,所以也许这就是我需要去的地方。 – JoshDoody 2013-03-27 19:11:00