OSDN Git Service

Rails 2.3.11
[redminele/redminele.git] / ruby / lib / ruby / gems / 1.8 / gems / activerecord-2.3.11 / lib / active_record / autosave_association.rb
diff --git a/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.11/lib/active_record/autosave_association.rb b/ruby/lib/ruby/gems/1.8/gems/activerecord-2.3.11/lib/active_record/autosave_association.rb
new file mode 100644 (file)
index 0000000..045e16b
--- /dev/null
@@ -0,0 +1,395 @@
+module ActiveRecord
+  # AutosaveAssociation is a module that takes care of automatically saving
+  # your associations when the parent is saved. In addition to saving, it
+  # also destroys any associations that were marked for destruction.
+  # (See mark_for_destruction and marked_for_destruction?)
+  #
+  # Saving of the parent, its associations, and the destruction of marked
+  # associations, all happen inside 1 transaction. This should never leave the
+  # database in an inconsistent state after, for instance, mass assigning
+  # attributes and saving them.
+  #
+  # If validations for any of the associations fail, their error messages will
+  # be applied to the parent.
+  #
+  # Note that it also means that associations marked for destruction won't
+  # be destroyed directly. They will however still be marked for destruction.
+  #
+  # === One-to-one Example
+  #
+  # Consider a Post model with one Author:
+  #
+  #   class Post
+  #     has_one :author, :autosave => true
+  #   end
+  #
+  # Saving changes to the parent and its associated model can now be performed
+  # automatically _and_ atomically:
+  #
+  #   post = Post.find(1)
+  #   post.title # => "The current global position of migrating ducks"
+  #   post.author.name # => "alloy"
+  #
+  #   post.title = "On the migration of ducks"
+  #   post.author.name = "Eloy Duran"
+  #
+  #   post.save
+  #   post.reload
+  #   post.title # => "On the migration of ducks"
+  #   post.author.name # => "Eloy Duran"
+  #
+  # Destroying an associated model, as part of the parent's save action, is as
+  # simple as marking it for destruction:
+  #
+  #   post.author.mark_for_destruction
+  #   post.author.marked_for_destruction? # => true
+  #
+  # Note that the model is _not_ yet removed from the database:
+  #   id = post.author.id
+  #   Author.find_by_id(id).nil? # => false
+  #
+  #   post.save
+  #   post.reload.author # => nil
+  #
+  # Now it _is_ removed from the database:
+  #   Author.find_by_id(id).nil? # => true
+  #
+  # === One-to-many Example
+  #
+  # Consider a Post model with many Comments:
+  #
+  #   class Post
+  #     has_many :comments, :autosave => true
+  #   end
+  #
+  # Saving changes to the parent and its associated model can now be performed
+  # automatically _and_ atomically:
+  #
+  #   post = Post.find(1)
+  #   post.title # => "The current global position of migrating ducks"
+  #   post.comments.first.body # => "Wow, awesome info thanks!"
+  #   post.comments.last.body # => "Actually, your article should be named differently."
+  #
+  #   post.title = "On the migration of ducks"
+  #   post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
+  #
+  #   post.save
+  #   post.reload
+  #   post.title # => "On the migration of ducks"
+  #   post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
+  #
+  # Destroying one of the associated models members, as part of the parent's
+  # save action, is as simple as marking it for destruction:
+  #
+  #   post.comments.last.mark_for_destruction
+  #   post.comments.last.marked_for_destruction? # => true
+  #   post.comments.length # => 2
+  #
+  # Note that the model is _not_ yet removed from the database:
+  #   id = post.comments.last.id
+  #   Comment.find_by_id(id).nil? # => false
+  #
+  #   post.save
+  #   post.reload.comments.length # => 1
+  #
+  # Now it _is_ removed from the database:
+  #   Comment.find_by_id(id).nil? # => true
+  #
+  # === Validation
+  #
+  # Validation is performed on the parent as usual, but also on all autosave
+  # enabled associations. If any of the associations fail validation, its
+  # error messages will be applied on the parents errors object and validation
+  # of the parent will fail.
+  #
+  # Consider a Post model with Author which validates the presence of its name
+  # attribute:
+  #
+  #   class Post
+  #     has_one :author, :autosave => true
+  #   end
+  #
+  #   class Author
+  #     validates_presence_of :name
+  #   end
+  #
+  #   post = Post.find(1)
+  #   post.author.name = ''
+  #   post.save # => false
+  #   post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
+  #
+  # No validations will be performed on the associated models when validations
+  # are skipped for the parent:
+  #
+  #   post = Post.find(1)
+  #   post.author.name = ''
+  #   post.save(false) # => true
+  module AutosaveAssociation
+    ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
+
+    def self.included(base)
+      base.class_eval do
+        base.extend(ClassMethods)
+        alias_method_chain :reload, :autosave_associations
+
+        ASSOCIATION_TYPES.each do |type|
+          base.send("valid_keys_for_#{type}_association") << :autosave
+        end
+      end
+    end
+
+    module ClassMethods
+      private
+
+      # def belongs_to(name, options = {})
+      #   super
+      #   add_autosave_association_callbacks(reflect_on_association(name))
+      # end
+      ASSOCIATION_TYPES.each do |type|
+        module_eval <<-CODE, __FILE__, __LINE__ + 1
+          def #{type}(name, options = {})
+            super
+            add_autosave_association_callbacks(reflect_on_association(name))
+          end
+        CODE
+      end
+
+      # Adds a validate and save callback for the association as specified by
+      # the +reflection+.
+      #
+      # For performance reasons, we don't check whether to validate at runtime,
+      # but instead only define the method and callback when needed. However,
+      # this can change, for instance, when using nested attributes, which is
+      # called _after_ the association has been defined. Since we don't want
+      # the callbacks to get defined multiple times, there are guards that
+      # check if the save or validation methods have already been defined
+      # before actually defining them.
+      def add_autosave_association_callbacks(reflection)
+        save_method = :"autosave_associated_records_for_#{reflection.name}"
+        validation_method = :"validate_associated_records_for_#{reflection.name}"
+        collection = reflection.collection?
+
+        unless method_defined?(save_method)
+          if collection
+            before_save :before_save_collection_association
+
+            define_method(save_method) { save_collection_association(reflection) }
+            # Doesn't use after_save as that would save associations added in after_create/after_update twice
+            after_create save_method
+            after_update save_method
+          else
+            if reflection.macro == :has_one
+              define_method(save_method) { save_has_one_association(reflection) }
+              after_save save_method
+            else
+              define_method(save_method) { save_belongs_to_association(reflection) }
+              before_save save_method
+            end
+          end
+        end
+
+        if reflection.validate? && !method_defined?(validation_method)
+          method = (collection ? :validate_collection_association : :validate_single_association)
+          define_method(validation_method) { send(method, reflection) }
+          validate validation_method
+        end
+      end
+    end
+
+    # Reloads the attributes of the object as usual and removes a mark for destruction.
+    def reload_with_autosave_associations(options = nil)
+      @marked_for_destruction = false
+      reload_without_autosave_associations(options)
+    end
+
+    # Marks this record to be destroyed as part of the parents save transaction.
+    # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
+    #
+    # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
+    def mark_for_destruction
+      @marked_for_destruction = true
+    end
+
+    # Returns whether or not this record will be destroyed as part of the parents save transaction.
+    #
+    # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
+    def marked_for_destruction?
+      @marked_for_destruction
+    end
+
+    # Returns whether or not this record has been changed in any way (including whether
+    # any of its nested autosave associations are likewise changed)
+    def changed_for_autosave?
+      new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave?
+    end
+    
+    private
+
+    # Returns the record for an association collection that should be validated
+    # or saved. If +autosave+ is +false+ only new records will be returned,
+    # unless the parent is/was a new record itself.
+    def associated_records_to_validate_or_save(association, new_record, autosave)
+      if new_record
+        association
+      elsif autosave
+        association.target.select { |record| record.changed_for_autosave? }
+      else
+        association.target.select { |record| record.new_record? }
+      end
+    end
+    
+    # go through nested autosave associations that are loaded in memory (without loading
+    # any new ones), and return true if is changed for autosave
+    def nested_records_changed_for_autosave?
+      self.class.reflect_on_all_autosave_associations.each do |reflection|
+        if association = association_instance_get(reflection.name)
+          if [:belongs_to, :has_one].include?(reflection.macro)
+            return true if association.target && association.target.changed_for_autosave?
+          else
+            association.target.each {|record| return true if record.changed_for_autosave? }
+          end
+        end
+      end
+      false
+    end
+    
+    # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+    # turned on for the association specified by +reflection+.
+    def validate_single_association(reflection)
+      if (association = association_instance_get(reflection.name)) && !association.target.nil?
+        association_valid?(reflection, association)
+      end
+    end
+
+    # Validate the associated records if <tt>:validate</tt> or
+    # <tt>:autosave</tt> is turned on for the association specified by
+    # +reflection+.
+    def validate_collection_association(reflection)
+      if association = association_instance_get(reflection.name)
+        if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+          records.each { |record| association_valid?(reflection, record) }
+        end
+      end
+    end
+
+    # Returns whether or not the association is valid and applies any errors to
+    # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+    # enabled records if they're marked_for_destruction? or destroyed.
+    def association_valid?(reflection, association)
+      return true if association.destroyed? || association.marked_for_destruction?
+
+      unless valid = association.valid?
+        if reflection.options[:autosave]
+          association.errors.each_error do |attribute, error|
+            attribute = "#{reflection.name}.#{attribute}"
+            errors.add(attribute, error.dup) unless errors.on(attribute)
+          end
+        else
+          errors.add(reflection.name)
+        end
+      end
+      valid
+    end
+
+    # Is used as a before_save callback to check while saving a collection
+    # association whether or not the parent was a new record before saving.
+    def before_save_collection_association
+      @new_record_before_save = new_record?
+      true
+    end
+
+    # Saves any new associated records, or all loaded autosave associations if
+    # <tt>:autosave</tt> is enabled on the association.
+    #
+    # In addition, it destroys all children that were marked for destruction
+    # with mark_for_destruction.
+    #
+    # This all happens inside a transaction, _if_ the Transactions module is included into
+    # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+    def save_collection_association(reflection)
+      if association = association_instance_get(reflection.name)
+        autosave = reflection.options[:autosave]
+
+        if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+          records.each do |record|
+            next if record.destroyed?
+
+            if autosave && record.marked_for_destruction?
+              association.destroy(record)
+            elsif autosave != false && (@new_record_before_save || record.new_record?)
+              if autosave
+                saved = association.send(:insert_record, record, false, false)
+              else
+                association.send(:insert_record, record)
+              end
+            elsif autosave
+              saved = record.save(false)
+            end
+
+            raise ActiveRecord::Rollback if saved == false
+          end
+        end
+
+        # reconstruct the SQL queries now that we know the owner's id
+        association.send(:construct_sql) if association.respond_to?(:construct_sql)
+      end
+    end
+
+    # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+    # on the association.
+    #
+    # In addition, it will destroy the association if it was marked for
+    # destruction with mark_for_destruction.
+    #
+    # This all happens inside a transaction, _if_ the Transactions module is included into
+    # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+    def save_has_one_association(reflection)
+      if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
+        autosave = reflection.options[:autosave]
+
+        if autosave && association.marked_for_destruction?
+          association.destroy
+        else
+          key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+          if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
+            association[reflection.primary_key_name] = key
+            saved = association.save(!autosave)
+            raise ActiveRecord::Rollback if !saved && autosave
+            saved
+          end
+        end
+      end
+    end
+
+    # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+    # on the association.
+    #
+    # In addition, it will destroy the association if it was marked for
+    # destruction with mark_for_destruction.
+    #
+    # This all happens inside a transaction, _if_ the Transactions module is included into
+    # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+    def save_belongs_to_association(reflection)
+      if (association = association_instance_get(reflection.name)) && !association.destroyed?
+        autosave = reflection.options[:autosave]
+
+        if autosave && association.marked_for_destruction?
+          association.destroy
+        elsif autosave != false
+          saved = association.save(!autosave) if association.new_record? || autosave
+
+          if association.updated?
+            association_id = association.send(reflection.options[:primary_key] || :id)
+            self[reflection.primary_key_name] = association_id
+            # TODO: Removing this code doesn't seem to matter…
+            if reflection.options[:polymorphic]
+              self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
+            end
+          end
+
+          saved if autosave
+        end
+      end
+    end
+  end
+end