5

Mój problem polega na tym, że napotkaliśmy ograniczenia dotyczące accepts_nested_attributes_for, więc muszę wymyślić, jak samodzielnie powielić tę funkcję, aby uzyskać większą elastyczność. (Zobacz poniżej, co dokładnie mnie zawiesza.) Moje pytanie brzmi: Jak powinna wyglądać moja forma, kontroler i modele, jeśli chcę je usprawniać i zwiększać accepts_nested_attributes_for? Prawdziwą sztuczką jest to, że muszę mieć możliwość aktualizacji zarówno istniejących, jak i nowych modeli, z istniejącymi powiązaniami/atrybutami.Railsy - Jak zarządzać zagnieżdżonymi atrybutami bez użycia accepts_nested_attributes_for?

Buduję aplikację korzystającą z zagnieżdżonych formularzy. Początkowo użyłem tego RailsCast jako wzorca (leveraging accepts_nested_attributes_for): Railscast 196: Nested Model Form.

Moja aplikacja to listy kontrolne zawierające zadania (zadania) i pozwalam użytkownikowi zaktualizować listę kontrolną (nazwisko, opis) oraz dodać/usunąć powiązane zadania w jednym formularzu. Działa to dobrze, ale napotykam problemy, gdy włączam to do innego aspektu mojej aplikacji: historii poprzez wersjonowanie.

Duża część mojej aplikacji polega na tym, że muszę zapisywać informacje historyczne dotyczące moich modeli i skojarzeń. Zakończyłem przetasowanie własnej wersji (here to moje pytanie, w którym opisuję mój proces decyzyjny/rozważania), a duża część to przepływ pracy, w którym muszę utworzyć nową wersję starej rzeczy, wprowadzać aktualizacje do nowej wersji , zarchiwizuj starą wersję. Jest to niewidoczne dla użytkownika, który postrzega to doświadczenie jako proste aktualizowanie modelu za pomocą interfejsu użytkownika.

Code - modele

#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 - aktualna forma (UWAGA: @jobs jest zdefiniowany jako niezarchiwizowane pracy dla tej listy kontrolnej w edycji akcji kontrolera listy kontrolne, tak jest @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 %> 

Code - snippet from checklists_controller.rb # Aktualizacja

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 tu właśnie biegnę nto accepts_nested_attributes_for ograniczenia (jest to dobrze udokumentowane here. Otrzymuję komunikat "Nie można znaleźć modelu 1 z identyfikatorem = X dla modelu 2 z ID = Y", który jest zasadniczo taki sam jak zaprojektowany.

Jak więc utworzyć wiele modeli zagnieżdżonych i dodać/usunąć je w formularzu modelu macierzystego, podobnego do tego, co akceptuje_nested_attributes_do, ale czy mogę to zrobić samodzielnie?

Opcje, które widziałem - są jednym z tych najlepszych? Prawdziwa sztuczka polega na tym, że muszę mieć możliwość aktualizacji zarówno istniejących, jak i nowych modeli z istniejącymi powiązaniami/atrybutami. Nie mogę ich połączyć, więc po prostu je nazwę.

Redtape (na github) Virtus (również github)

Dzięki za pomoc!

+0

Jeśli to rozwiązałeś, byłbym bardzo zainteresowany obejrzeniem twojego rozwiązania. –

+0

Mario, rozwiązałem to, a mój kod został opublikowany poniżej. To nie jest świetny kod, ale jeśli walczysz z czymś podobnym, może to da ci kilka pomysłów. Wszelkie pytania, po prostu komentarz tutaj lub na moją odpowiedź, a ja postaram się wyjaśnić, czy mogę. – JoshDoody

Odpowiedz

1

Od Mario skomentował moje pytanie i zapytał, czy nie rozwiązać go, myślałem, że dzielę rozwiązanie.

Muszę powiedzieć, że jestem pewien, że nie jest to bardzo eleganckie rozwiązanie i nie jest to świetny kod. Ale właśnie to wymyśliłem i działa. Ponieważ to pytanie jest dość techniczne, nie publikuję tutaj pseudokodu - publikuję pełny kod zarówno dla modelu listy kontrolnej, jak i dla akcji aktualizacji kontrolera Checklists (części kodu, które mają zastosowanie do tego pytania). Jestem również całkiem pewien, że moje bloki transakcji nie robią nic (muszę to naprawić).

Podstawową ideą jest ręczne wykonanie aktualizacji. Zamiast polegać na update_attributes (i accepts_nested_attributes_for), ręcznie zaktualizować listę kontrolną w dwóch fazach:

  1. Czy rzeczywistą zmianę obiektu checklist (lista kontrolna ma tylko nazwę i opis)? Jeśli tak, utwórz nową listę kontrolną, spraw, by nowa była dzieckiem starej i ustaw nową dla wszystkich dodanych lub wybranych zadań.
  2. Jeśli sama lista kontrolna się nie zmieniła (nazwa i opis pozostały takie same), czy przypisane do niej zadania uległy zmianie? Jeśli tak, zarchiwizuj przydziały zadań, które zostały usunięte i dodaj nowe przydziały zadań.

Istnieje kilka "podległych" rzeczy, które uważam za bezpieczne do zignorowania (logiczne jest ustalenie, czy ma to znaczenie nawet w przypadku, gdy lista kontrolna się zmieniła - jeśli nie ma żadnych zgłoszeń (zapisy historycznych danych z listy kontrolnej) po prostu zaktualizuj listę kontrolną w miejscu, nie wykonując żadnej z tych operacji archiwizowania lub dodawania/odejmowania zadań).

Nie wiem, czy to będzie pomocne, ale tutaj tak jest.

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 (sterownik)

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 - postać kontrolna

<%= 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, powinieneś unikać bezpośredniego manipulowania parametrami '_destroy'. Uważam to za szczegół implementacji, który przepuszcza, aby klient mógł wykonywać pracę za pośrednictwem javascript. Na serwerze użyj 'mark_for_destruction' i' marked_for_destruction? 'Zobacz http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html –

5

Prawdopodobnie będziesz chciał wyrwać złożone rzeczy accepts_nested i utworzyć niestandardową klasę lub moduł, który zawiera wszystkie wymagane kroki.

Istnieje kilka przydatnych rzeczy na tym stanowisku

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

Szczególnie punkt 3

+0

Myślę, że masz rację. Widziałem już ten post na blogu i myślę, że to właśnie zainspirowało ten klejnot, o którym wspomniałem powyżej. Pytałam gdzie indziej, a redtape pojawiła się kilka razy, więc może właśnie tam muszę iść. – JoshDoody