5

Il mio problema è che ho incontrato limitazioni di accept_nested_attributes_for, quindi ho bisogno di capire come replicare questa funzionalità da solo per avere maggiore flessibilità. (Vedi sotto per esattamente cosa mi sta impiccando.) Quindi la mia domanda è: quale dovrebbe essere il mio modulo, controller e modelli se voglio mimmic e augment accept_nested_attributes_for? Il vero trucco è che devo essere in grado di aggiornare sia i nuovi modelli esistenti che quelli esistenti con associazioni/attributi.Rails - Come gestire gli attributi nidificati senza utilizzare accept_nested_attributes_for?

Sto costruendo un'app che utilizza moduli annidati. Inizialmente ho usato questo RailsCast come progetto (sfruttando accept_nested_attributes_for): Railscast 196: Nested Model Form.

La mia app è liste di controllo con lavori (attività), e sto permettendo all'utente di aggiornare la lista di controllo (nome, descrizione) e aggiungere/rimuovere lavori associati in un unico modulo. Funziona bene, ma mi imbatto in problemi quando lo incorporo in un altro aspetto della mia app: la cronologia tramite il controllo delle versioni.

Gran parte della mia app è che ho bisogno di registrare informazioni storiche per i miei modelli e associazioni. Ho finito con il mio sistema di versioning (here è la mia domanda in cui descrivo il mio processo/considerazioni decisionali), e gran parte di questo è un flusso di lavoro in cui ho bisogno di creare una nuova versione di una cosa vecchia, apportare aggiornamenti alla nuova versione , archivia la vecchia versione. Questo è invisibile all'utente, che vede l'esperienza semplicemente aggiornando un modello attraverso l'interfaccia utente.

Code - modelli

#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 

Codice - forma attuale (NOTA: @jobs è definito come i lavori non archiviati per questa lista di controllo nel regolatore liste di controllo Modifica azione, così è @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 %> 

Codice: snippet from checklists_controller.rb # Aggiornamento

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

E questo è dove corro nto il accept_nested_attributes_for limitation (è documentato abbastanza bene here. Ottengo l'eccezione "Impossibile trovare Model1 con ID = X per Model2 con ID = Y", che è fondamentalmente come progettato.

Quindi, come posso creare più modelli nidificati e aggiungerli/rimuoverli sul modulo del modello principale in modo simile a ciò che accetta_nested_attributes_for fa, ma da solo?

Le opzioni che ho visto sono tra le migliori? Il vero trucco è che devo essere in grado di aggiornare sia i nuovi modelli esistenti che quelli esistenti con associazioni/attributi. Non riesco a collegarli, quindi li chiamerò semplicemente.

Redtape (su GitHub) Virtus (anche github)

Grazie per il vostro aiuto!

+0

Se l'hai risolto sarei molto interessato a vedere la tua soluzione. –

+0

Mario, l'ho risolto e ho pubblicato il mio codice qui sotto. Non è un grande codice, ma se stai lottando con qualcosa di simile, forse ti darà qualche idea. Qualsiasi domanda, basta commentare qui o sulla mia risposta e cercherò di chiarire se posso. – JoshDoody

risposta

1

Da Mario ha commentato la mia domanda e mi chiese se ho risolto, ho pensato di condividere la mia soluzione.

Devo dire che sono sicuro che questa non è una soluzione molto elegante, e non è un gran bel codice. Ma è quello che mi è venuto in mente, e funziona. Poiché questa domanda è piuttosto tecnica, non sto pubblicando pseudo-codice qui - sto pubblicando il codice completo sia per il modello Checklist che per l'azione di aggiornamento del controller Checklist (le parti del codice che si applicano a questa domanda, comunque). Sono anche abbastanza sicuro che i miei blocchi di transazioni non stiano facendo nulla (ho bisogno di aggiustarli).

L'idea di base è che ho scomposto manualmente l'azione di aggiornamento. Piuttosto che basarsi su update_attributes (e accepts_nested_attributes_for), aggiorno manualmente l'elenco di controllo in due fasi:

  1. Forse il cambiamento reale oggetto lista di controllo (un elenco di controllo ha solo un nome e descrizione)? In caso affermativo, crea una nuova lista di controllo, rendi la nuova una figlia di una vecchia e imposta quella nuova con qualunque lavoro sia stato aggiunto o selezionato per essa.
  2. Se l'elenco di controllo di per sé non cambiava (il nome e la descrizione rimanevano gli stessi), i lavori ad esso assegnati cambiano? In caso affermativo, archivia le assegnazioni di lavoro rimosse e aggiungi eventuali nuovi incarichi di lavoro.

Ci sono alcune cose "sottomissione" che penso sia sicuro di ignorare qui (è fondamentalmente logica per determinare se è importante anche come la lista di controllo modificata - se non ci sono osservazioni (registrazioni di dati storici di una lista di controllo) quindi basta aggiornare la lista di controllo in posizione senza eseguire alcuna archiviazione o aggiungere/sottrarre attività di lavoro).

Non so se questo sarà utile, ma eccolo lo è comunque.

Codice - checklist.rb (modello)

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 

Codice - checklists_controller.rb (controllore)

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 

Codice - Lista di controllo sotto forma

<%= 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, dovresti evitare di manipolare direttamente i parametri '_destroy'. Lo considero come un dettaglio di implementazione che sanguina, in modo che il client possa eseguire il lavoro tramite javascript. Sul server, usa "mark_for_destruction" e "marked_for_destruction?" Vedi http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html –

5

Probabilmente vorrete estrarre il complesso accetta le cose_necessario e creare una classe o un modulo personalizzato per contenere tutti i passaggi richiesti.

Ci sono alcune cose utili in questo post

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

In particolare il punto 3

+0

Penso che tu abbia ragione. Ho visto quel post sul blog prima, e penso che sia in realtà ciò che ha ispirato la gemma redtape che ho menzionato sopra. Ho chiesto in giro altrove, e redtape è uscito un paio di volte, quindi forse è lì che devo andare. – JoshDoody

Problemi correlati