Merged updates from trunk into stable branch
[feedcatcher.git] / vendor / rails / activerecord / lib / active_record / autosave_association.rb
diff --git a/vendor/rails/activerecord/lib/active_record/autosave_association.rb b/vendor/rails/activerecord/lib/active_record/autosave_association.rb
new file mode 100644 (file)
index 0000000..741aa2a
--- /dev/null
@@ -0,0 +1,349 @@
+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 %{
+          def #{type}(name, options = {})
+            super
+            add_autosave_association_callbacks(reflect_on_association(name))
+          end
+        }
+      end
+
+      # Adds a validate and save callback for the association as specified by
+      # the +reflection+.
+      def add_autosave_association_callbacks(reflection)
+        save_method = "autosave_associated_records_for_#{reflection.name}"
+        validation_method = "validate_associated_records_for_#{reflection.name}"
+        validate validation_method
+
+        case reflection.macro
+        when :has_many, :has_and_belongs_to_many
+          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
+
+          define_method(validation_method) { validate_collection_association(reflection) }
+        else
+          case reflection.macro
+          when :has_one
+            define_method(save_method) { save_has_one_association(reflection) }
+            after_save save_method
+          when :belongs_to
+            define_method(save_method) { save_belongs_to_association(reflection) }
+            before_save save_method
+          end
+          define_method(validation_method) { validate_single_association(reflection) }
+        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
+
+    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 association.loaded?
+        autosave ? association : association.select { |record| record.new_record? }
+      else
+        autosave ? association.target : association.target.select { |record| record.new_record? }
+      end
+    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 reflection.options[:validate] == true || reflection.options[:autosave] == true
+        if (association = association_instance_get(reflection.name)) && !association.target.nil?
+          association_valid?(reflection, association)
+        end
+      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 reflection.options[:validate] != false && 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?.
+    def association_valid?(reflection, association)
+      unless valid = association.valid?
+        if reflection.options[:autosave]
+          unless association.marked_for_destruction?
+            association.errors.each do |attribute, message|
+              attribute = "#{reflection.name}_#{attribute}"
+              errors.add(attribute, message) unless errors.on(attribute)
+            end
+          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|
+            if autosave && record.marked_for_destruction?
+              association.destroy(record)
+            elsif @new_record_before_save || record.new_record?
+              if autosave
+                association.send(:insert_record, record, false, false)
+              else
+                association.send(:insert_record, record)
+              end
+            elsif autosave
+              record.save(false)
+            end
+          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?
+        if reflection.options[:autosave] && association.marked_for_destruction?
+          association.destroy
+        elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave]
+          association[reflection.primary_key_name] = id
+          association.save(false)
+        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)
+        if reflection.options[:autosave] && association.marked_for_destruction?
+          association.destroy
+        else
+          association.save(false) if association.new_record? || reflection.options[:autosave]
+
+          if association.updated?
+            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
+        end
+      end
+    end
+  end
+end
\ No newline at end of file