Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Octocat-spinner-32-eaf2f5

Cannot retrieve contributors at this time

file 124 lines (105 sloc) 3.904 kb
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
# NestedAssignment
module NestedAssignment
  include RecursionControl

  def self.included(base)
    base.class_eval do
      extend ClassMethods
      
      alias_method_chain :create_or_update, :associated
      alias_method_chain :valid?, :associated
# alias_method_chain :changed?, :associated
    end
  end

  module ClassMethods
    # Parallels attr_accessible. Could easily trigger from an association option (e.g. :accessible => true)
    # or even from attr_accessible itself (cool!).
    def accessible_associations(*associations)
      associations.each do |name|
      
        # singular associations
        if [:belongs_to, :has_one].include? self.reflect_on_association(name).macro
          define_method("#{name}_params=") do |row|
            assoc = self.send(name)
            
            if row[:_delete].to_s == "1"
              [assoc].detect{|r| r.id == row[:id].to_i}._delete = true if row[:id]
            else
              record = row[:id].blank? ? assoc.build : [assoc].detect{|r| r.id == row[:id].to_i}
              record.attributes = row.except(:id, :_delete)
            end
          end
        # plural collections
        else
          define_method("#{name}_params=") do |hash|
            assoc = self.send(name)
            
            hash.values.each do |row|
              if row[:_delete].to_s == "1"
                assoc.detect{|r| r.id == row[:id].to_i}._delete = true if row[:id]
              else
                record = row[:id].blank? ? assoc.build : assoc.detect{|r| r.id == row[:id].to_i}
                record.attributes = row.except(:id, :_delete)
              end
            end
          end
        end
                
      end
    end
  
    def association_names
      @association_names ||= reflect_on_all_associations.map(&:name)
    end
  end
  
  # marks the (associated) record to be deleted in the next deep save
  attr_accessor :_delete
  
  # deep validation of any changed (existing) records.
  # makes sure that any single invalid record will not halt the
  # validation process, so that all errors will be available
  # afterwards.
  def valid_with_associated?(*args)
    without_recursion(:valid?) do
      [modified_associated.all?(&:valid?), valid_without_associated?(*args)].all?
    end
  end
  
  # deep saving of any new, changed, or deleted records.
  def create_or_update_with_associated(*args)
    without_recursion(:create_or_update){
    self.class.transaction do
      create_or_update_without_associated(*args) &&
        modified_associated.all?{|a| a.save(*args)} &&
        deletable_associated.all?{|a| a.destroy}
    end
    }
  end
  
  # Without this, we may not save deeply nested and changed records.
  # For example, suppose that User -> Task -> Tags, and that we change
  # an attribute on a tag but not on the task. Then when we are saving
  # the user, we would want to say that the task had changed so we
  # could then recurse and discover that the tag had changed.
  #
  # Unfortunately, this can also have a 2x performance penalty.
  def changed_with_associated?
    without_recursion(:changed) do
      changed_without_associated? or changed_associated
    end
  end
  
  protected
  
  def deletable_associated
    instantiated_associated.select{|a| a._delete}
  end

  def modified_associated
    instantiated_associated.select{|a| a.changed? and !a.new_record? and not a.id_changed?}
  end

  def changed_associated
    instantiated_associated.select{|a| a.changed?}
  end

  def instantiated_associated
    instantiated = []
    self.class.association_names.each do |name|
      ivar = "@#{name}"
      if association = instance_variable_get(ivar)
        if association.target.is_a?(Array)
          instantiated.concat association.target
        elsif association.target
          instantiated << association.target
        end
      end
    end
    instantiated
  end

end
Something went wrong with that request. Please try again.