Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #1152: dirty.diff

File dirty.diff, 13.7 kB (added by bitsweat, 3 years ago)
  • activerecord/test/change_tracking_test.rb

    old new  
     1require 'abstract_unit' 
     2require 'fixtures/company' 
     3 
     4Company.track_changes! 
     5 
     6class ChangeTrackingTest < Test::Unit::TestCase 
     7  self.use_instantiated_fixtures = false 
     8  fixtures :companies 
     9 
     10  def setup 
     11    @company = Company.find_first 
     12  end 
     13 
     14  def test_instance_variable_for_new_company 
     15    company = Company.new 
     16    assert_nil company.instance_variable_get('@attributes_snapshot') 
     17    assert_valid_instance_variable_lifecycle_for company 
     18  end 
     19 
     20  def test_instance_variable_for_company_after_find 
     21    assert_nil @company.instance_variable_get('@attributes_snapshot') 
     22    assert_valid_instance_variable_lifecycle_for @company 
     23  end 
     24 
     25  def test_instance_variable_for_company_after_save 
     26    clone = @company.clone 
     27    clone.save 
     28    assert_valid_instance_variable_lifecycle_for clone 
     29  end 
     30 
     31  def test_lifecycle 
     32    # Clean. 
     33    assert @company.changed_attribute_names.empty? 
     34    assert !@company.name_changed?     
     35 
     36    # Dirty. 
     37    @company.name = 'Fu' 
     38    assert @company.name_changed? 
     39    assert_equal 1, @company.changed_attribute_names.size 
     40 
     41    # Clean. 
     42    @company.save 
     43    assert !@company.name_changed? 
     44    assert @company.changed_attribute_names.empty? 
     45  end 
     46 
     47  def test_tracking_in_place_changes 
     48    # Dirty an attribute without using a setter method. 
     49    assert !@company.name_changed?     
     50    @company.name << ' Inc.' 
     51    assert @company.name_changed? 
     52  end 
     53 
     54  def test_dirty_collections 
     55    @company.name = 'Fu' 
     56    [:changed, :dirty, :modified].each do |prefix| 
     57      assert_respond_to @company, "#{prefix}_attribute_names" 
     58      assert_equal ['name'], @company.send("#{prefix}_attribute_names") 
     59    end 
     60  end 
     61 
     62  def test_dirty_queries 
     63    @company.name = 'Fu' 
     64    assert_respond_to @company, :name_changed? 
     65    assert @company.name_changed? 
     66    assert_respond_to @company, :name_dirty? 
     67    assert @company.name_dirty? 
     68    assert_respond_to @company, :name_modified? 
     69    assert @company.name_modified? 
     70  end 
     71 
     72  protected 
     73    def assert_valid_instance_variable_lifecycle_for(company) 
     74      # nil until requested.  Pull saved state from db. 
     75      # Mind your transaction isolation. 
     76      company.name = 'foo' 
     77      company.name_changed? 
     78      snapshot = company.instance_variable_get('@attributes_snapshot') 
     79      assert_not_nil snapshot 
     80      assert_kind_of Hash, snapshot 
     81 
     82      # Clear snapshot after save. 
     83      assert company.save 
     84      assert_equal company.attributes_before_type_cast, company.instance_variable_get('@attributes_snapshot') 
     85    end 
     86end 
  • activerecord/test/callbacks_test.rb

    old new  
    286286    ], david.history 
    287287  end 
    288288end 
     289 
     290 
     291module WithAfterSave 
     292  def self.append_features(base) 
     293    base.after_save do |model| 
     294      model.history << [:after_save, :declared] 
     295    end 
     296  end 
     297end 
     298 
     299class ActiveRecord::Base 
     300  def self.with_after_save 
     301    include WithAfterSave 
     302  end 
     303end 
     304 
     305class AfterSaveDev < CallbackDeveloper 
     306  with_after_save 
     307end 
     308 
     309class SubDev < CallbackDeveloper 
     310  private 
     311    def foobar 
     312      history << [:after_save, :declared] 
     313    end 
     314end 
     315SubDev.after_save :foobar 
     316 
     317class CallbacksTest < Test::Unit::TestCase 
     318  def setup 
     319    @developers = create_fixtures('developers') 
     320  end 
     321 
     322  def test_declared_after_save 
     323    [AfterSaveDev.find(1), SubDev.find(1)].each do |david| 
     324      david.save 
     325      assert_equal [ 
     326        [ :after_find,            :string ], 
     327        [ :after_find,            :proc   ], 
     328        [ :after_find,            :object ], 
     329        [ :after_find,            :block  ], 
     330        [ :after_initialize,            :string ], 
     331        [ :after_initialize,            :proc   ], 
     332        [ :after_initialize,            :object ], 
     333        [ :after_initialize,            :block  ], 
     334        [ :before_validation,           :string ], 
     335        [ :before_validation,           :proc   ], 
     336        [ :before_validation,           :object ], 
     337        [ :before_validation,           :block  ], 
     338        [ :before_validation_on_update, :string ], 
     339        [ :before_validation_on_update, :proc   ], 
     340        [ :before_validation_on_update, :object ], 
     341        [ :before_validation_on_update, :block  ], 
     342        [ :after_validation,            :string ], 
     343        [ :after_validation,            :proc   ], 
     344        [ :after_validation,            :object ], 
     345        [ :after_validation,            :block  ], 
     346        [ :after_validation_on_update,  :string ], 
     347        [ :after_validation_on_update,  :proc   ], 
     348        [ :after_validation_on_update,  :object ], 
     349        [ :after_validation_on_update,  :block  ], 
     350        [ :before_save,                 :string ], 
     351        [ :before_save,                 :proc   ], 
     352        [ :before_save,                 :object ], 
     353        [ :before_save,                 :block  ], 
     354        [ :before_update,               :string ], 
     355        [ :before_update,               :proc   ], 
     356        [ :before_update,               :object ], 
     357        [ :before_update,               :block  ], 
     358        [ :after_update,                :string ], 
     359        [ :after_update,                :proc   ], 
     360        [ :after_update,                :object ], 
     361        [ :after_update,                :block  ], 
     362        [ :after_save,                  :string ], 
     363        [ :after_save,                  :proc   ], 
     364        [ :after_save,                  :object ], 
     365        [ :after_save,                  :block  ], 
     366        [ :after_save,                  :declared ] 
     367      ], david.history 
     368    end 
     369  end 
     370end 
  • activerecord/test/associations_go_eager_test.rb

    old new  
    3535 
    3636  def test_eager_association_loading_with_belongs_to 
    3737    comments = Comment.find(:all, :include => :post) 
    38     assert_equal @welcome.title, comments.first.post.title 
    39     assert_equal @thinking.title, comments.last.post.title 
     38    comments.each do |comment| 
     39      assert_kind_of Post, comment.instance_variable_get('@post') 
     40    end 
    4041  end 
    4142 
    4243  def test_eager_association_loading_with_habtm 
  • activerecord/lib/active_record/change_tracking.rb

    old new  
     1module ActiveRecord 
     2  class Base 
     3    # Enable change tracking for this class and its subclasses. 
     4    def self.track_changes! 
     5      include ChangeTracking 
     6    end 
     7  end 
     8 
     9  # Track unsaved attribute changes by comparing against "clean" attribute 
     10  # snapshots taken when records are initialized and saved. 
     11  # 
     12  # - changed_attribute_names returns the names of changed attributes. 
     13  # - #{attr}_changed? methods query whether attr has changed. 
     14  # - changed? queries whether any attribute has changed. 
     15  # 
     16  # This implementation is a non-invasive wrapper for existing Active Record 
     17  # behavior.  If it's generally useful, it should be merged into AR::Base 
     18  # for better performance. 
     19  module ChangeTracking 
     20    def self.append_features(base) 
     21      super 
     22      base.extend(ClassMethods) 
     23 
     24      base.class_eval do 
     25        # Wrap save to snapshot new attributes state. 
     26        alias_method :save_without_change_tracking, :save 
     27        alias_method :save, :save_with_change_tracking 
     28 
     29        # Wrap method_missing to respond to #{attr}_changed? queries. 
     30        alias_method :method_missing_without_change_tracking, :method_missing 
     31        alias_method :method_missing, :method_missing_with_change_tracking 
     32 
     33        # Some alternate names. 
     34        alias_method :dirty_attribute_names, :changed_attribute_names 
     35        alias_method :modified_attribute_names, :changed_attribute_names 
     36      end 
     37    end 
     38 
     39    module ClassMethods 
     40      def self.extend_object(base) 
     41        super 
     42        s = class << base; self; end 
     43        s.send :alias_method, :column_methods_hash_without_dirty_tracking, :column_methods_hash 
     44        s.send :alias_method, :column_methods_hash, :column_methods_hash_with_dirty_tracking 
     45      end 
     46 
     47      # Wrap column_methods_hash to include the #{attr}_changed? method family. 
     48      def column_methods_hash_with_dirty_tracking 
     49        @dynamic_methods_hash ||= columns_hash.keys.inject(column_methods_hash_without_dirty_tracking) do |methods, attr| 
     50          methods["#{attr}_changed?".to_sym] = true 
     51          methods["#{attr}_dirty?".to_sym] = true 
     52          methods["#{attr}_modified?".to_sym] = true 
     53          methods 
     54        end 
     55      end 
     56    end 
     57 
     58    # Names of attributes which have changed since the last clean snapshot. 
     59    def changed_attribute_names 
     60      attribute_names.select { |attr| attribute_changed? attr } 
     61    end 
     62 
     63    # Has an attribute changed since the last clean snapshot? 
     64    def attribute_changed?(attr = nil) 
     65      # Has any attribute changed? 
     66      if attr.nil? 
     67        attribute_names.any? { |attr| attribute_changed? attr } 
     68 
     69      # Has this attribute changed? 
     70      else 
     71        attributes_snapshot[attr.to_s] != @attributes[attr.to_s] 
     72      end 
     73    end 
     74 
     75    def save_with_change_tracking(*args) 
     76      result = save_without_change_tracking(*args) 
     77      @attributes_snapshot = attributes_before_type_cast.freeze 
     78      result 
     79    end 
     80 
     81    private 
     82      # Wrap method_missing to add #{attr}_changed? methods. 
     83      def method_missing_with_change_tracking(method, *args, &block) 
     84        if method.to_s =~ /^([a-zA-Z][-_\w]*)_(changed|dirty|modified)\?$/i 
     85          attribute_changed?($1) 
     86        else 
     87          method_missing_without_change_tracking(method, *args, &block) 
     88        end 
     89      end 
     90 
     91      # Lazy-load the record's saved state for comparisons.  The extra 
     92      # query for a few records is faster overall than dirty tracking 
     93      # all records (cloning is expensive.) 
     94      def attributes_snapshot 
     95        @attributes_snapshot ||=  if new_record? 
     96                                    EMPTY_ATTRIBUTES_SNAPSHOT 
     97                                  else 
     98                                    self.class.find(self.id).attributes_before_type_cast.freeze 
     99                                  end 
     100      end 
     101 
     102      EMPTY_ATTRIBUTES_SNAPSHOT = HashWithIndifferentAccess.new.freeze 
     103  end 
     104end 
  • activerecord/lib/active_record/base.rb

    old new  
    10691069 
    10701070      # Returns a hash of all the attributes with their names as keys and clones of their objects as values. 
    10711071      def attributes 
    1072         self.attribute_names.inject({}) do |attributes, name| 
    1073           begin 
    1074             attributes[name] = read_attribute(name).clone 
    1075           rescue TypeError, NoMethodError 
    1076             attributes[name] = read_attribute(name) 
    1077           end 
    1078           attributes 
    1079         end 
     1072        clone_attributes :read_attribute 
    10801073      end 
    10811074 
     1075      # A hash of cloned attributes before type-casting and unserialization. 
     1076      def attributes_before_type_cast 
     1077        clone_attributes :read_attribute_before_type_cast 
     1078      end 
     1079 
    10821080      # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither 
    10831081      # nil nor empty? (the latter only applies to objects that responds to empty?, most notably Strings). 
    10841082      def attribute_present?(attribute) 
     
    14041403      def has_yaml_encoding_header?(string) 
    14051404        string[0..3] == "--- " 
    14061405      end 
     1406 
     1407      # A hash of cloned attributes pulled with reader_method. 
     1408      def clone_attributes(reader_method, attributes = HashWithIndifferentAccess.new) 
     1409        self.attribute_names.inject(attributes) do |attributes, name| 
     1410          attributes[name] = clone_attribute_value(reader_method, name) 
     1411          attributes 
     1412        end 
     1413      end 
     1414 
     1415      # Clone a single attribute value. 
     1416      def clone_attribute_value(reader_method, attribute_name) 
     1417        value = send(reader_method, attribute_name) 
     1418        value.clone 
     1419      rescue TypeError, NoMethodError 
     1420        value 
     1421      end 
    14071422  end 
    14081423end 
  • activerecord/lib/active_record.rb

    old new  
    4646require 'active_record/acts/nested_set' 
    4747require 'active_record/locking' 
    4848require 'active_record/migration' 
     49require 'active_record/change_tracking' 
    4950 
    5051ActiveRecord::Base.class_eval do 
    5152  include ActiveRecord::Validations