Ruby on rails Rails-如何在不使用accepts\u nested\u attributes\u的情况下管理嵌套属性?

Ruby on rails Rails-如何在不使用accepts\u nested\u attributes\u的情况下管理嵌套属性?,ruby-on-rails,nested-forms,nested-attributes,model-associations,update-attributes,Ruby On Rails,Nested Forms,Nested Attributes,Model Associations,Update Attributes,我的问题是,我遇到了接受\u嵌套的\u属性\u的限制,因此我需要找出如何自己复制该功能,以获得更大的灵活性。(我的问题是:如果我想mimic和扩充accepts_nested_attributes_,那么我的表单、控制器和模型应该是什么样子?真正的诀窍是我需要能够使用现有关联/属性更新现有模型和新模型。 我正在构建一个使用嵌套表单的应用程序。我最初将此RailsCast用作蓝图(利用嵌套属性): 我的应用程序是带有作业(任务)的检查表,我让用户更新检查表(名称、说明)并以单一形式添加/删除相关作

我的问题是,我遇到了接受\u嵌套的\u属性\u的限制,因此我需要找出如何自己复制该功能,以获得更大的灵活性。(我的问题是:如果我想mimic和扩充accepts_nested_attributes_,那么我的表单、控制器和模型应该是什么样子?真正的诀窍是我需要能够使用现有关联/属性更新现有模型和新模型。

我正在构建一个使用嵌套表单的应用程序。我最初将此RailsCast用作蓝图(利用嵌套属性):

我的应用程序是带有作业(任务)的检查表,我让用户更新检查表(名称、说明)并以单一形式添加/删除相关作业。这很管用,但当我将其整合到我的应用程序的另一个方面:通过版本控制的历史时,我遇到了问题

我的应用程序的很大一部分是我需要记录我的模型和关联的历史信息。我最终推出了自己的版本控制(这是我描述决策过程/考虑事项的问题),其中很大一部分是一个工作流,我需要创建旧事物的新版本,更新新版本,归档旧版本。这对于用户来说是不可见的,用户将体验视为通过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
这就是我遇到accepts_nested_attributes_for Limition的原因(它被很好地记录了下来。我得到了“找不到ID=X的Model1,ID=Y的Model2”异常,这基本上与设计一样

那么,我如何创建多个嵌套模型,并在父模型的表单上添加/删除它们,就像接受\u嵌套的\u属性\u所做的一样,但这是我自己做的

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

繁文缛节(在github上) Virtus(也叫github)


感谢您的帮助!

您可能希望删除复杂的accepts\u嵌套内容,并创建一个自定义类或模块,以包含所需的所有步骤

这篇文章里有一些有用的东西


特别是第3点

既然马里奥对我的问题发表了评论,问我是否解决了这个问题,我想我会分享我的解决方案

我应该说,我确信这不是一个非常优雅的解决方案,也不是很好的代码。但这正是我提出的,而且它是有效的。因为这个问题非常技术性,所以我不会在这里发布伪代码-我将发布清单模型和清单控制器更新操作的完整代码(不管怎样,代码中适用于这个问题的部分)。我也非常确定我的事务块实际上没有做任何事情(我需要修复它们)

基本思想是我手动执行更新操作。我不依赖更新属性(并接受嵌套属性),而是分两个阶段手动更新检查表:

  • 实际的检查表对象是否发生了变化(检查表只有名称和说明)?如果发生了变化,请创建一个新的检查表,使新的检查表成为旧检查表的子检查表,并使用为其添加或选择的任何作业设置新检查表
  • 如果检查表本身没有更改(名称和描述保持不变),则分配给它的作业是否更改?如果更改,则归档已删除的作业分配,并添加任何新的作业分配
  • 这里有一些我认为可以安全忽略的“提交”内容(基本上是确定检查表如何更改是否重要的逻辑——如果没有任何提交(检查表历史数据的记录),那么只需在不进行任何存档或添加/删除作业的情况下更新检查表即可)

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

    代码-checklist.rb(型号)

    类清单s)}
    属于\u-to:creator,:class\u-name=>“User”,:foreign\u-key=>“creator\u-id”
    你有很多意见书吗
    有多个:检查表\u作业,:依赖=>:destroy,:order=>'检查表\u作业。作业\u位置',:条件=>{'archived\u at'=>nil}
    有许多:作业,:至=>:检查表\u作业
    有很多:未归档的作业,:至=>:检查表\u作业,
    :source=>:作业,
    :conditions=>['checklists\u jobs.archived=?',false],:order=>'checklists\u jobs.job\u position'
    有多个工作日:检查表:依赖=>:销毁
    有多个:工作日,:至=>:检查表\u工作日
    def make_child_of(旧检查表)
    self.parent\u id=(old\u checklist.parent\u id==0)?old\u checklist.id:old\u checklist.parent\u id
    self.previous\u id=旧的\u.id
    self.version=(旧版本+1)
    结束
    def设置新作业(新作业)
    新的工作要做,每个人都要做|
    自归档的作业
    1,:占位符=>'命名列表…',:类=>'自动Resizer检查表名称'%>
    1,:占位符=>'可选描述…',:类=>'自动Resizer'> {:class=>'forminline'}do | j |%> j%> “btn btn主节点”%> 'btn'> 从作业库添加作业 “btn btn主节点”%> 'btn'>
    我认为你是对的。我以前看过那篇博文,我认为它实际上是我上面提到的繁文缛节宝石的灵感来源。我一直在其他地方打听,繁文缛节已经出现了好几次,所以也许这就是我需要去的地方。如果你解决了这个问题,我会非常有兴趣看到你的解决方案。马里奥,我确实解决了,a我把我的代码贴在下面。这不是很好的代码,但如果你正在为类似的事情而挣扎,也许是
    <%= 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 %>
    
    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])
    
    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
    
    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 %>