Ticket #1152: dirty.diff
| File dirty.diff, 13.7 kB (added by bitsweat, 3 years ago) |
|---|
-
activerecord/test/change_tracking_test.rb
old new 1 require 'abstract_unit' 2 require 'fixtures/company' 3 4 Company.track_changes! 5 6 class 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 86 end -
activerecord/test/callbacks_test.rb
old new 286 286 ], david.history 287 287 end 288 288 end 289 290 291 module WithAfterSave 292 def self.append_features(base) 293 base.after_save do |model| 294 model.history << [:after_save, :declared] 295 end 296 end 297 end 298 299 class ActiveRecord::Base 300 def self.with_after_save 301 include WithAfterSave 302 end 303 end 304 305 class AfterSaveDev < CallbackDeveloper 306 with_after_save 307 end 308 309 class SubDev < CallbackDeveloper 310 private 311 def foobar 312 history << [:after_save, :declared] 313 end 314 end 315 SubDev.after_save :foobar 316 317 class 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 370 end -
activerecord/test/associations_go_eager_test.rb
old new 35 35 36 36 def test_eager_association_loading_with_belongs_to 37 37 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 40 41 end 41 42 42 43 def test_eager_association_loading_with_habtm -
activerecord/lib/active_record/change_tracking.rb
old new 1 module 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 104 end -
activerecord/lib/active_record/base.rb
old new 1069 1069 1070 1070 # Returns a hash of all the attributes with their names as keys and clones of their objects as values. 1071 1071 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 1080 1073 end 1081 1074 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 1082 1080 # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither 1083 1081 # nil nor empty? (the latter only applies to objects that responds to empty?, most notably Strings). 1084 1082 def attribute_present?(attribute) … … 1404 1403 def has_yaml_encoding_header?(string) 1405 1404 string[0..3] == "--- " 1406 1405 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 1407 1422 end 1408 1423 end -
activerecord/lib/active_record.rb
old new 46 46 require 'active_record/acts/nested_set' 47 47 require 'active_record/locking' 48 48 require 'active_record/migration' 49 require 'active_record/change_tracking' 49 50 50 51 ActiveRecord::Base.class_eval do 51 52 include ActiveRecord::Validations