8

私の問題は、accepts_nested_attributes_forの制限に遭遇したことです。そのため、柔軟性を高めるために、その機能を自分で複製する方法を理解する必要があります。(正確に私を悩ませているものについては、以下を参照してください。)したがって、私の質問は、accepts_nested_attributes_forを模倣および拡張したい場合、フォーム、コントローラー、およびモデルはどのように見えるべきですか?本当の秘訣は、既存のモデルと新しいモデルの両方を既存の関連付け/属性で更新できるようにする必要があることです。

ネストされたフォームを使用するアプリを作成しています。私は当初、このRailsCastを青写真として使用しました(accepts_nested_attributes_forを活用):Railscast 196:ネストされたモデルフォーム

私のアプリはジョブ(タスク)を含むチェックリストであり、ユーザーがチェックリスト(名前、説明)を更新し、関連するジョブを1つのフォームで追加/削除できるようにします。これはうまく機能しますが、これをアプリの別の側面、つまりバージョン管理による履歴に組み込むと、問題が発生します。

私のアプリの大きな部分は、モデルと関連付けの履歴情報を記録する必要があることです。私は自分のバージョン管理をロールバックすることになりました(これが私の決定プロセス/考慮事項を説明する私の質問です)、そしてその大部分は古いものの新しいバージョンを作成し、新しいバージョンを更新する必要があるワークフローです、古いバージョンをアーカイブします。これは、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#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=YのModel2のID=XのModel1が見つかりませんでした」という例外が発生します。

では、複数のネストされたモデルを作成し、accepts_nested_attributes_forと同様に、親モデルのフォームでそれらを追加/削除するにはどうすればよいですか?

私が見たオプション-これらの最高の1つですか?本当の秘訣は、既存のモデルと新しいモデルの両方を既存の関連付け/属性で更新できるようにする必要があることです。リンクできないので、名前を付けます。

Redtape(github上)Virtus(またgithub)

ご協力いただきありがとうございます!

4

2 に答える 2

5

おそらく、複雑なaccepts_nestedのものを取り除いて、必要なすべてのステップを含むカスタムクラスまたはモジュールを作成したいと思うでしょう。

この投稿にはいくつかの便利なものがあります

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

特にポイント3

于 2013-03-27T00:04:41.507 に答える
2

マリオが私の質問にコメントして、私がそれを解決するかどうか尋ねたので、私は私の解決策を共有しようと思いました。

これは非常に洗練されたソリューションではなく、優れたコードではないと確信しています。しかし、それは私が思いついたものであり、それは機能します。この質問はかなり技術的なものなので、ここでは擬似コードを投稿していません。チェックリストモデルとチェックリストコントローラーの更新アクション(とにかく、この質問に適用されるコードの部分)の両方の完全なコードを投稿しています。また、トランザクションブロックが実際には何もしていないこともかなり確信しています(これらを修正する必要があります)。

基本的な考え方は、更新アクションを手動で実行することです。update_attributes(およびaccepts_nested_attributes_for)に依存するのではなく、次の2つのフェーズでチェックリストを手動で更新します。

  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 %>
于 2013-07-04T06:41:47.913 に答える