mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Concernify AR AttributeMethods
This commit is contained in:
parent
2c2ca833a5
commit
c2b075bed0
10 changed files with 451 additions and 383 deletions
|
@ -52,7 +52,6 @@ module ActiveRecord
|
||||||
autoload :Batches, 'active_record/batches'
|
autoload :Batches, 'active_record/batches'
|
||||||
autoload :Calculations, 'active_record/calculations'
|
autoload :Calculations, 'active_record/calculations'
|
||||||
autoload :Callbacks, 'active_record/callbacks'
|
autoload :Callbacks, 'active_record/callbacks'
|
||||||
autoload :Dirty, 'active_record/dirty'
|
|
||||||
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
|
autoload :DynamicFinderMatch, 'active_record/dynamic_finder_match'
|
||||||
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
|
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
|
||||||
autoload :Migration, 'active_record/migration'
|
autoload :Migration, 'active_record/migration'
|
||||||
|
@ -71,6 +70,15 @@ module ActiveRecord
|
||||||
autoload :Transactions, 'active_record/transactions'
|
autoload :Transactions, 'active_record/transactions'
|
||||||
autoload :Validations, 'active_record/validations'
|
autoload :Validations, 'active_record/validations'
|
||||||
|
|
||||||
|
module AttributeMethods
|
||||||
|
autoload :BeforeTypeCast, 'active_record/attribute_methods/before_type_cast'
|
||||||
|
autoload :Dirty, 'active_record/attribute_methods/dirty'
|
||||||
|
autoload :Query, 'active_record/attribute_methods/query'
|
||||||
|
autoload :Read, 'active_record/attribute_methods/read'
|
||||||
|
autoload :TimeZoneConversion, 'active_record/attribute_methods/time_zone_conversion'
|
||||||
|
autoload :Write, 'active_record/attribute_methods/write'
|
||||||
|
end
|
||||||
|
|
||||||
module Locking
|
module Locking
|
||||||
autoload :Optimistic, 'active_record/locking/optimistic'
|
autoload :Optimistic, 'active_record/locking/optimistic'
|
||||||
autoload :Pessimistic, 'active_record/locking/pessimistic'
|
autoload :Pessimistic, 'active_record/locking/pessimistic'
|
||||||
|
|
|
@ -4,20 +4,11 @@ module ActiveRecord
|
||||||
module AttributeMethods #:nodoc:
|
module AttributeMethods #:nodoc:
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
|
|
||||||
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
|
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
|
||||||
|
|
||||||
included do
|
included do
|
||||||
attribute_method_suffix(*DEFAULT_SUFFIXES)
|
|
||||||
|
|
||||||
cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
|
cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
|
||||||
self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
|
self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
|
||||||
|
|
||||||
cattr_accessor :time_zone_aware_attributes, :instance_writer => false
|
|
||||||
self.time_zone_aware_attributes = false
|
|
||||||
|
|
||||||
class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
|
|
||||||
self.skip_time_zone_conversion_for_attributes = []
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Declare and check for suffixed attribute methods.
|
# Declare and check for suffixed attribute methods.
|
||||||
|
@ -60,42 +51,32 @@ module ActiveRecord
|
||||||
@@attribute_method_regexp.match(method_name)
|
@@attribute_method_regexp.match(method_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# Contains the names of the generated attribute methods.
|
# Contains the names of the generated attribute methods.
|
||||||
def generated_methods #:nodoc:
|
def generated_methods #:nodoc:
|
||||||
@generated_methods ||= Set.new
|
@generated_methods ||= Set.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def generated_methods?
|
def generated_methods?
|
||||||
!generated_methods.empty?
|
!generated_methods.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generates all the attribute related methods for columns in the database
|
# Generates all the attribute related methods for columns in the database
|
||||||
# accessors, mutators and query methods.
|
# accessors, mutators and query methods.
|
||||||
def define_attribute_methods
|
def define_attribute_methods
|
||||||
return if generated_methods?
|
return if generated_methods?
|
||||||
columns_hash.each do |name, column|
|
columns_hash.keys.each do |name|
|
||||||
unless instance_method_already_implemented?(name)
|
# TODO: Generate for all defined suffixes
|
||||||
if self.serialized_attributes[name]
|
["", "=", "?"].each do |suffix|
|
||||||
define_read_method_for_serialized_attribute(name)
|
method_name = "#{name}#{suffix}"
|
||||||
elsif create_time_zone_conversion_attribute?(name, column)
|
unless instance_method_already_implemented?(method_name)
|
||||||
define_read_method_for_time_zone_conversion(name)
|
generate_method = "define_attribute_method#{suffix}"
|
||||||
else
|
if respond_to?(generate_method)
|
||||||
define_read_method(name.to_sym, name, column)
|
send(generate_method, name)
|
||||||
|
else
|
||||||
|
evaluate_attribute_method(name, "def #{method_name}(*args); attribute#{suffix}('#{name}', *args); end", method_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
unless instance_method_already_implemented?("#{name}=")
|
|
||||||
if create_time_zone_conversion_attribute?(name, column)
|
|
||||||
define_write_method_for_time_zone_conversion(name)
|
|
||||||
else
|
|
||||||
define_write_method(name.to_sym)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
unless instance_method_already_implemented?("#{name}?")
|
|
||||||
define_question_method(name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -104,14 +85,12 @@ module ActiveRecord
|
||||||
# method is defined by Active Record though.
|
# method is defined by Active Record though.
|
||||||
def instance_method_already_implemented?(method_name)
|
def instance_method_already_implemented?(method_name)
|
||||||
method_name = method_name.to_s
|
method_name = method_name.to_s
|
||||||
return true if method_name =~ /^id(=$|\?$|$)/
|
return true if method_name =~ /^id(=$|\?$|$)/ # TODO: Check against all defined suffixes
|
||||||
@_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set
|
@_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map {|m| m.to_s }.to_set
|
||||||
@@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map{|m| m.to_s }.to_set
|
@@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map{|m| m.to_s }.to_set
|
||||||
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
|
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
|
||||||
@_defined_class_methods.include?(method_name)
|
@_defined_class_methods.include?(method_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
alias :define_read_methods :define_attribute_methods
|
|
||||||
|
|
||||||
# +cache_attributes+ allows you to declare which converted attribute values should
|
# +cache_attributes+ allows you to declare which converted attribute values should
|
||||||
# be cached. Usually caching only pays off for attributes with expensive conversion
|
# be cached. Usually caching only pays off for attributes with expensive conversion
|
||||||
|
@ -133,83 +112,18 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Suffixes a, ?, c become regexp /(a|\?|c)$/
|
# Suffixes a, ?, c become regexp /(a|\?|c)$/
|
||||||
def rebuild_attribute_method_regexp
|
def rebuild_attribute_method_regexp
|
||||||
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
|
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
|
||||||
@@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
|
@@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
# Default to =, ?, _before_type_cast
|
|
||||||
def attribute_method_suffixes
|
def attribute_method_suffixes
|
||||||
@@attribute_method_suffixes ||= []
|
@@attribute_method_suffixes ||= []
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_time_zone_conversion_attribute?(name, column)
|
|
||||||
time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Define an attribute reader method. Cope with nil column.
|
|
||||||
def define_read_method(symbol, attr_name, column)
|
|
||||||
cast_code = column.type_cast_code('v') if column
|
|
||||||
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
|
|
||||||
|
|
||||||
unless attr_name.to_s == self.primary_key.to_s
|
|
||||||
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
|
|
||||||
end
|
|
||||||
|
|
||||||
if cache_attribute?(attr_name)
|
|
||||||
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
|
||||||
end
|
|
||||||
evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Define read method for serialized attribute.
|
|
||||||
def define_read_method_for_serialized_attribute(attr_name)
|
|
||||||
evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
|
||||||
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
|
|
||||||
def define_read_method_for_time_zone_conversion(attr_name)
|
|
||||||
method_body = <<-EOV
|
|
||||||
def #{attr_name}(reload = false)
|
|
||||||
cached = @attributes_cache['#{attr_name}']
|
|
||||||
return cached if cached && !reload
|
|
||||||
time = read_attribute('#{attr_name}')
|
|
||||||
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
|
||||||
end
|
|
||||||
EOV
|
|
||||||
evaluate_attribute_method attr_name, method_body
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defines a predicate method <tt>attr_name?</tt>.
|
|
||||||
def define_question_method(attr_name)
|
|
||||||
evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
|
|
||||||
end
|
|
||||||
|
|
||||||
def define_write_method(attr_name)
|
|
||||||
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
|
|
||||||
end
|
|
||||||
|
|
||||||
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
|
||||||
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
|
|
||||||
def define_write_method_for_time_zone_conversion(attr_name)
|
|
||||||
method_body = <<-EOV
|
|
||||||
def #{attr_name}=(time)
|
|
||||||
unless time.acts_like?(:time)
|
|
||||||
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
|
|
||||||
end
|
|
||||||
time = time.in_time_zone rescue nil if time
|
|
||||||
write_attribute(:#{attr_name}, time)
|
|
||||||
end
|
|
||||||
EOV
|
|
||||||
evaluate_attribute_method attr_name, method_body, "#{attr_name}="
|
|
||||||
end
|
|
||||||
|
|
||||||
# Evaluate the definition for an attribute related method
|
# Evaluate the definition for an attribute related method
|
||||||
def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
|
def evaluate_attribute_method(attr_name, method_definition, method_name)
|
||||||
|
|
||||||
unless method_name.to_s == primary_key.to_s
|
unless method_name.to_s == primary_key.to_s
|
||||||
generated_methods << method_name
|
generated_methods << method_name
|
||||||
end
|
end
|
||||||
|
@ -225,8 +139,7 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # ClassMethods
|
end
|
||||||
|
|
||||||
|
|
||||||
# Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
|
# Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
|
||||||
# were first-class methods. So a Person class with a name attribute can use Person#name and
|
# were first-class methods. So a Person class with a name attribute can use Person#name and
|
||||||
|
@ -248,7 +161,7 @@ module ActiveRecord
|
||||||
return self.send(method_id, *args, &block)
|
return self.send(method_id, *args, &block)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
guard_private_attribute_method!(method_name, args)
|
guard_private_attribute_method!(method_name, args)
|
||||||
if self.class.primary_key.to_s == method_name
|
if self.class.primary_key.to_s == method_name
|
||||||
id
|
id
|
||||||
|
@ -266,80 +179,6 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
|
||||||
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
|
||||||
def read_attribute(attr_name)
|
|
||||||
attr_name = attr_name.to_s
|
|
||||||
if !(value = @attributes[attr_name]).nil?
|
|
||||||
if column = column_for_attribute(attr_name)
|
|
||||||
if unserializable_attribute?(attr_name, column)
|
|
||||||
unserialize_attribute(attr_name)
|
|
||||||
else
|
|
||||||
column.type_cast(value)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
else
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_attribute_before_type_cast(attr_name)
|
|
||||||
@attributes[attr_name]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns true if the attribute is of a text column and marked for serialization.
|
|
||||||
def unserializable_attribute?(attr_name, column)
|
|
||||||
column.text? && self.class.serialized_attributes[attr_name]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the unserialized object of the attribute.
|
|
||||||
def unserialize_attribute(attr_name)
|
|
||||||
unserialized_object = object_from_yaml(@attributes[attr_name])
|
|
||||||
|
|
||||||
if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
|
|
||||||
@attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
|
|
||||||
else
|
|
||||||
raise SerializationTypeMismatch,
|
|
||||||
"#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
|
|
||||||
# columns are turned into +nil+.
|
|
||||||
def write_attribute(attr_name, value)
|
|
||||||
attr_name = attr_name.to_s
|
|
||||||
@attributes_cache.delete(attr_name)
|
|
||||||
if (column = column_for_attribute(attr_name)) && column.number?
|
|
||||||
@attributes[attr_name] = convert_number_column_value(value)
|
|
||||||
else
|
|
||||||
@attributes[attr_name] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
def query_attribute(attr_name)
|
|
||||||
unless value = read_attribute(attr_name)
|
|
||||||
false
|
|
||||||
else
|
|
||||||
column = self.class.columns_hash[attr_name]
|
|
||||||
if column.nil?
|
|
||||||
if Numeric === value || value !~ /[^0-9]/
|
|
||||||
!value.to_i.zero?
|
|
||||||
else
|
|
||||||
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
|
||||||
!value.blank?
|
|
||||||
end
|
|
||||||
elsif column.number?
|
|
||||||
!value.zero?
|
|
||||||
else
|
|
||||||
!value.blank?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
|
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
|
||||||
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
|
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
|
||||||
# which will all return +true+.
|
# which will all return +true+.
|
||||||
|
@ -358,7 +197,7 @@ module ActiveRecord
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if @attributes.nil?
|
if @attributes.nil?
|
||||||
return super
|
return super
|
||||||
elsif @attributes.include?(method_name)
|
elsif @attributes.include?(method_name)
|
||||||
|
@ -376,24 +215,9 @@ module ActiveRecord
|
||||||
raise NoMethodError.new("Attempt to call private method", method_name, args)
|
raise NoMethodError.new("Attempt to call private method", method_name, args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def missing_attribute(attr_name, stack)
|
def missing_attribute(attr_name, stack)
|
||||||
raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handle *? for method_missing.
|
|
||||||
def attribute?(attribute_name)
|
|
||||||
query_attribute(attribute_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle *= for method_missing.
|
|
||||||
def attribute=(attribute_name, value)
|
|
||||||
write_attribute(attribute_name, value)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle *_before_type_cast for method_missing.
|
|
||||||
def attribute_before_type_cast(attribute_name)
|
|
||||||
read_attribute_before_type_cast(attribute_name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module AttributeMethods
|
||||||
|
module BeforeTypeCast
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
attribute_method_suffix "_before_type_cast"
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_attribute_before_type_cast(attr_name)
|
||||||
|
@attributes[attr_name]
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Handle *_before_type_cast for method_missing.
|
||||||
|
def attribute_before_type_cast(attribute_name)
|
||||||
|
read_attribute_before_type_cast(attribute_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
187
activerecord/lib/active_record/attribute_methods/dirty.rb
Normal file
187
activerecord/lib/active_record/attribute_methods/dirty.rb
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module AttributeMethods
|
||||||
|
# Track unsaved attribute changes.
|
||||||
|
#
|
||||||
|
# A newly instantiated object is unchanged:
|
||||||
|
# person = Person.find_by_name('uncle bob')
|
||||||
|
# person.changed? # => false
|
||||||
|
#
|
||||||
|
# Change the name:
|
||||||
|
# person.name = 'Bob'
|
||||||
|
# person.changed? # => true
|
||||||
|
# person.name_changed? # => true
|
||||||
|
# person.name_was # => 'uncle bob'
|
||||||
|
# person.name_change # => ['uncle bob', 'Bob']
|
||||||
|
# person.name = 'Bill'
|
||||||
|
# person.name_change # => ['uncle bob', 'Bill']
|
||||||
|
#
|
||||||
|
# Save the changes:
|
||||||
|
# person.save
|
||||||
|
# person.changed? # => false
|
||||||
|
# person.name_changed? # => false
|
||||||
|
#
|
||||||
|
# Assigning the same value leaves the attribute unchanged:
|
||||||
|
# person.name = 'Bill'
|
||||||
|
# person.name_changed? # => false
|
||||||
|
# person.name_change # => nil
|
||||||
|
#
|
||||||
|
# Which attributes have changed?
|
||||||
|
# person.name = 'bob'
|
||||||
|
# person.changed # => ['name']
|
||||||
|
# person.changes # => { 'name' => ['Bill', 'bob'] }
|
||||||
|
#
|
||||||
|
# Before modifying an attribute in-place:
|
||||||
|
# person.name_will_change!
|
||||||
|
# person.name << 'by'
|
||||||
|
# person.name_change # => ['uncle bob', 'uncle bobby']
|
||||||
|
module Dirty
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
||||||
|
|
||||||
|
included do
|
||||||
|
attribute_method_suffix *DIRTY_SUFFIXES
|
||||||
|
|
||||||
|
alias_method_chain :save, :dirty
|
||||||
|
alias_method_chain :save!, :dirty
|
||||||
|
alias_method_chain :update, :dirty
|
||||||
|
alias_method_chain :reload, :dirty
|
||||||
|
|
||||||
|
superclass_delegating_accessor :partial_updates
|
||||||
|
self.partial_updates = true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Do any attributes have unsaved changes?
|
||||||
|
# person.changed? # => false
|
||||||
|
# person.name = 'bob'
|
||||||
|
# person.changed? # => true
|
||||||
|
def changed?
|
||||||
|
!changed_attributes.empty?
|
||||||
|
end
|
||||||
|
|
||||||
|
# List of attributes with unsaved changes.
|
||||||
|
# person.changed # => []
|
||||||
|
# person.name = 'bob'
|
||||||
|
# person.changed # => ['name']
|
||||||
|
def changed
|
||||||
|
changed_attributes.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
# Map of changed attrs => [original value, new value].
|
||||||
|
# person.changes # => {}
|
||||||
|
# person.name = 'bob'
|
||||||
|
# person.changes # => { 'name' => ['bill', 'bob'] }
|
||||||
|
def changes
|
||||||
|
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Attempts to +save+ the record and clears changed attributes if successful.
|
||||||
|
def save_with_dirty(*args) #:nodoc:
|
||||||
|
if status = save_without_dirty(*args)
|
||||||
|
changed_attributes.clear
|
||||||
|
end
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
||||||
|
def save_with_dirty!(*args) #:nodoc:
|
||||||
|
status = save_without_dirty!(*args)
|
||||||
|
changed_attributes.clear
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
# <tt>reload</tt> the record and clears changed attributes.
|
||||||
|
def reload_with_dirty(*args) #:nodoc:
|
||||||
|
record = reload_without_dirty(*args)
|
||||||
|
changed_attributes.clear
|
||||||
|
record
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Map of change <tt>attr => original value</tt>.
|
||||||
|
def changed_attributes
|
||||||
|
@changed_attributes ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||||
|
def attribute_changed?(attr)
|
||||||
|
changed_attributes.include?(attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle <tt>*_change</tt> for +method_missing+.
|
||||||
|
def attribute_change(attr)
|
||||||
|
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle <tt>*_was</tt> for +method_missing+.
|
||||||
|
def attribute_was(attr)
|
||||||
|
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
||||||
|
def attribute_will_change!(attr)
|
||||||
|
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Wrap write_attribute to remember original attribute value.
|
||||||
|
def write_attribute(attr, value)
|
||||||
|
attr = attr.to_s
|
||||||
|
|
||||||
|
# The attribute already has an unsaved change.
|
||||||
|
if changed_attributes.include?(attr)
|
||||||
|
old = changed_attributes[attr]
|
||||||
|
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
|
||||||
|
else
|
||||||
|
old = clone_attribute_value(:read_attribute, attr)
|
||||||
|
changed_attributes[attr] = old if field_changed?(attr, old, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Carry on.
|
||||||
|
super(attr, value)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_with_dirty
|
||||||
|
if partial_updates?
|
||||||
|
# Serialized attributes should always be written in case they've been
|
||||||
|
# changed in place.
|
||||||
|
update_without_dirty(changed | self.class.serialized_attributes.keys)
|
||||||
|
else
|
||||||
|
update_without_dirty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def field_changed?(attr, old, value)
|
||||||
|
if column = column_for_attribute(attr)
|
||||||
|
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
|
||||||
|
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
||||||
|
# Hence we don't record it as a change if the value changes from nil to ''.
|
||||||
|
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
||||||
|
# be typecast back to 0 (''.to_i => 0)
|
||||||
|
value = nil
|
||||||
|
else
|
||||||
|
value = column.type_cast(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
old != value
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def self.extended(base)
|
||||||
|
class << base
|
||||||
|
alias_method_chain :alias_attribute, :dirty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def alias_attribute_with_dirty(new_name, old_name)
|
||||||
|
alias_attribute_without_dirty(new_name, old_name)
|
||||||
|
DIRTY_SUFFIXES.each do |suffix|
|
||||||
|
module_eval <<-STR, __FILE__, __LINE__+1
|
||||||
|
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
||||||
|
STR
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
37
activerecord/lib/active_record/attribute_methods/query.rb
Normal file
37
activerecord/lib/active_record/attribute_methods/query.rb
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module AttributeMethods
|
||||||
|
module Query
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
attribute_method_suffix "?"
|
||||||
|
end
|
||||||
|
|
||||||
|
def query_attribute(attr_name)
|
||||||
|
unless value = read_attribute(attr_name)
|
||||||
|
false
|
||||||
|
else
|
||||||
|
column = self.class.columns_hash[attr_name]
|
||||||
|
if column.nil?
|
||||||
|
if Numeric === value || value !~ /[^0-9]/
|
||||||
|
!value.to_i.zero?
|
||||||
|
else
|
||||||
|
return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
|
||||||
|
!value.blank?
|
||||||
|
end
|
||||||
|
elsif column.number?
|
||||||
|
!value.zero?
|
||||||
|
else
|
||||||
|
!value.blank?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Handle *? for method_missing.
|
||||||
|
def attribute?(attribute_name)
|
||||||
|
query_attribute(attribute_name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
79
activerecord/lib/active_record/attribute_methods/read.rb
Normal file
79
activerecord/lib/active_record/attribute_methods/read.rb
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module AttributeMethods
|
||||||
|
module Read
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
# included do
|
||||||
|
# attribute_method_suffix ""
|
||||||
|
# end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
protected
|
||||||
|
def define_attribute_method(attr_name)
|
||||||
|
if self.serialized_attributes[attr_name]
|
||||||
|
define_read_method_for_serialized_attribute(attr_name)
|
||||||
|
else
|
||||||
|
define_read_method(attr_name.to_sym, attr_name, columns_hash[attr_name])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Define read method for serialized attribute.
|
||||||
|
def define_read_method_for_serialized_attribute(attr_name)
|
||||||
|
evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end", attr_name
|
||||||
|
end
|
||||||
|
|
||||||
|
# Define an attribute reader method. Cope with nil column.
|
||||||
|
def define_read_method(symbol, attr_name, column)
|
||||||
|
cast_code = column.type_cast_code('v') if column
|
||||||
|
access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
|
||||||
|
|
||||||
|
unless attr_name.to_s == self.primary_key.to_s
|
||||||
|
access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
|
||||||
|
end
|
||||||
|
|
||||||
|
if cache_attribute?(attr_name)
|
||||||
|
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
||||||
|
end
|
||||||
|
evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end", attr_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
|
||||||
|
# "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
|
||||||
|
def read_attribute(attr_name)
|
||||||
|
attr_name = attr_name.to_s
|
||||||
|
if !(value = @attributes[attr_name]).nil?
|
||||||
|
if column = column_for_attribute(attr_name)
|
||||||
|
if unserializable_attribute?(attr_name, column)
|
||||||
|
unserialize_attribute(attr_name)
|
||||||
|
else
|
||||||
|
column.type_cast(value)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if the attribute is of a text column and marked for serialization.
|
||||||
|
def unserializable_attribute?(attr_name, column)
|
||||||
|
column.text? && self.class.serialized_attributes[attr_name]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the unserialized object of the attribute.
|
||||||
|
def unserialize_attribute(attr_name)
|
||||||
|
unserialized_object = object_from_yaml(@attributes[attr_name])
|
||||||
|
|
||||||
|
if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
|
||||||
|
@attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
|
||||||
|
else
|
||||||
|
raise SerializationTypeMismatch,
|
||||||
|
"#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,60 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module AttributeMethods
|
||||||
|
module TimeZoneConversion
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
cattr_accessor :time_zone_aware_attributes, :instance_writer => false
|
||||||
|
self.time_zone_aware_attributes = false
|
||||||
|
|
||||||
|
class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
|
||||||
|
self.skip_time_zone_conversion_for_attributes = []
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
protected
|
||||||
|
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
||||||
|
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
|
||||||
|
def define_attribute_method(attr_name)
|
||||||
|
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
||||||
|
method_body = <<-EOV
|
||||||
|
def #{attr_name}(reload = false)
|
||||||
|
cached = @attributes_cache['#{attr_name}']
|
||||||
|
return cached if cached && !reload
|
||||||
|
time = read_attribute('#{attr_name}')
|
||||||
|
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
||||||
|
end
|
||||||
|
EOV
|
||||||
|
evaluate_attribute_method attr_name, method_body, attr_name
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
||||||
|
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
|
||||||
|
def define_attribute_method=(attr_name)
|
||||||
|
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
||||||
|
method_body = <<-EOV
|
||||||
|
def #{attr_name}=(time)
|
||||||
|
unless time.acts_like?(:time)
|
||||||
|
time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
|
||||||
|
end
|
||||||
|
time = time.in_time_zone rescue nil if time
|
||||||
|
write_attribute(:#{attr_name}, time)
|
||||||
|
end
|
||||||
|
EOV
|
||||||
|
evaluate_attribute_method attr_name, method_body, "#{attr_name}="
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def create_time_zone_conversion_attribute?(name, column)
|
||||||
|
time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
36
activerecord/lib/active_record/attribute_methods/write.rb
Normal file
36
activerecord/lib/active_record/attribute_methods/write.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
module ActiveRecord
|
||||||
|
module AttributeMethods
|
||||||
|
module Write
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
attribute_method_suffix "="
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
protected
|
||||||
|
def define_attribute_method=(attr_name)
|
||||||
|
evaluate_attribute_method attr_name, "def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", "#{attr_name}="
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
|
||||||
|
# columns are turned into +nil+.
|
||||||
|
def write_attribute(attr_name, value)
|
||||||
|
attr_name = attr_name.to_s
|
||||||
|
@attributes_cache.delete(attr_name)
|
||||||
|
if (column = column_for_attribute(attr_name)) && column.number?
|
||||||
|
@attributes[attr_name] = convert_number_column_value(value)
|
||||||
|
else
|
||||||
|
@attributes[attr_name] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
# Handle *= for method_missing.
|
||||||
|
def attribute=(attribute_name, value)
|
||||||
|
write_attribute(attribute_name, value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3212,7 +3212,9 @@ module ActiveRecord #:nodoc:
|
||||||
include Validations
|
include Validations
|
||||||
include Locking::Optimistic, Locking::Pessimistic
|
include Locking::Optimistic, Locking::Pessimistic
|
||||||
include AttributeMethods
|
include AttributeMethods
|
||||||
include Dirty
|
include AttributeMethods::Read, AttributeMethods::Write, AttributeMethods::BeforeTypeCast, AttributeMethods::Query
|
||||||
|
include AttributeMethods::TimeZoneConversion
|
||||||
|
include AttributeMethods::Dirty
|
||||||
include Callbacks, ActiveModel::Observing, Timestamp
|
include Callbacks, ActiveModel::Observing, Timestamp
|
||||||
include Associations, AssociationPreload, NamedScope
|
include Associations, AssociationPreload, NamedScope
|
||||||
include ActiveModel::Conversion
|
include ActiveModel::Conversion
|
||||||
|
|
|
@ -1,186 +0,0 @@
|
||||||
module ActiveRecord
|
|
||||||
# Track unsaved attribute changes.
|
|
||||||
#
|
|
||||||
# A newly instantiated object is unchanged:
|
|
||||||
# person = Person.find_by_name('uncle bob')
|
|
||||||
# person.changed? # => false
|
|
||||||
#
|
|
||||||
# Change the name:
|
|
||||||
# person.name = 'Bob'
|
|
||||||
# person.changed? # => true
|
|
||||||
# person.name_changed? # => true
|
|
||||||
# person.name_was # => 'uncle bob'
|
|
||||||
# person.name_change # => ['uncle bob', 'Bob']
|
|
||||||
# person.name = 'Bill'
|
|
||||||
# person.name_change # => ['uncle bob', 'Bill']
|
|
||||||
#
|
|
||||||
# Save the changes:
|
|
||||||
# person.save
|
|
||||||
# person.changed? # => false
|
|
||||||
# person.name_changed? # => false
|
|
||||||
#
|
|
||||||
# Assigning the same value leaves the attribute unchanged:
|
|
||||||
# person.name = 'Bill'
|
|
||||||
# person.name_changed? # => false
|
|
||||||
# person.name_change # => nil
|
|
||||||
#
|
|
||||||
# Which attributes have changed?
|
|
||||||
# person.name = 'bob'
|
|
||||||
# person.changed # => ['name']
|
|
||||||
# person.changes # => { 'name' => ['Bill', 'bob'] }
|
|
||||||
#
|
|
||||||
# Before modifying an attribute in-place:
|
|
||||||
# person.name_will_change!
|
|
||||||
# person.name << 'by'
|
|
||||||
# person.name_change # => ['uncle bob', 'uncle bobby']
|
|
||||||
module Dirty
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
|
||||||
|
|
||||||
included do
|
|
||||||
attribute_method_suffix *DIRTY_SUFFIXES
|
|
||||||
|
|
||||||
alias_method_chain :write_attribute, :dirty
|
|
||||||
alias_method_chain :save, :dirty
|
|
||||||
alias_method_chain :save!, :dirty
|
|
||||||
alias_method_chain :update, :dirty
|
|
||||||
alias_method_chain :reload, :dirty
|
|
||||||
|
|
||||||
superclass_delegating_accessor :partial_updates
|
|
||||||
self.partial_updates = true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Do any attributes have unsaved changes?
|
|
||||||
# person.changed? # => false
|
|
||||||
# person.name = 'bob'
|
|
||||||
# person.changed? # => true
|
|
||||||
def changed?
|
|
||||||
!changed_attributes.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
# List of attributes with unsaved changes.
|
|
||||||
# person.changed # => []
|
|
||||||
# person.name = 'bob'
|
|
||||||
# person.changed # => ['name']
|
|
||||||
def changed
|
|
||||||
changed_attributes.keys
|
|
||||||
end
|
|
||||||
|
|
||||||
# Map of changed attrs => [original value, new value].
|
|
||||||
# person.changes # => {}
|
|
||||||
# person.name = 'bob'
|
|
||||||
# person.changes # => { 'name' => ['bill', 'bob'] }
|
|
||||||
def changes
|
|
||||||
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
|
|
||||||
end
|
|
||||||
|
|
||||||
# Attempts to +save+ the record and clears changed attributes if successful.
|
|
||||||
def save_with_dirty(*args) #:nodoc:
|
|
||||||
if status = save_without_dirty(*args)
|
|
||||||
changed_attributes.clear
|
|
||||||
end
|
|
||||||
status
|
|
||||||
end
|
|
||||||
|
|
||||||
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
|
||||||
def save_with_dirty!(*args) #:nodoc:
|
|
||||||
status = save_without_dirty!(*args)
|
|
||||||
changed_attributes.clear
|
|
||||||
status
|
|
||||||
end
|
|
||||||
|
|
||||||
# <tt>reload</tt> the record and clears changed attributes.
|
|
||||||
def reload_with_dirty(*args) #:nodoc:
|
|
||||||
record = reload_without_dirty(*args)
|
|
||||||
changed_attributes.clear
|
|
||||||
record
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
# Map of change <tt>attr => original value</tt>.
|
|
||||||
def changed_attributes
|
|
||||||
@changed_attributes ||= {}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
|
||||||
def attribute_changed?(attr)
|
|
||||||
changed_attributes.include?(attr)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle <tt>*_change</tt> for +method_missing+.
|
|
||||||
def attribute_change(attr)
|
|
||||||
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle <tt>*_was</tt> for +method_missing+.
|
|
||||||
def attribute_was(attr)
|
|
||||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
|
||||||
def attribute_will_change!(attr)
|
|
||||||
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Wrap write_attribute to remember original attribute value.
|
|
||||||
def write_attribute_with_dirty(attr, value)
|
|
||||||
attr = attr.to_s
|
|
||||||
|
|
||||||
# The attribute already has an unsaved change.
|
|
||||||
if changed_attributes.include?(attr)
|
|
||||||
old = changed_attributes[attr]
|
|
||||||
changed_attributes.delete(attr) unless field_changed?(attr, old, value)
|
|
||||||
else
|
|
||||||
old = clone_attribute_value(:read_attribute, attr)
|
|
||||||
changed_attributes[attr] = old if field_changed?(attr, old, value)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Carry on.
|
|
||||||
write_attribute_without_dirty(attr, value)
|
|
||||||
end
|
|
||||||
|
|
||||||
def update_with_dirty
|
|
||||||
if partial_updates?
|
|
||||||
# Serialized attributes should always be written in case they've been
|
|
||||||
# changed in place.
|
|
||||||
update_without_dirty(changed | self.class.serialized_attributes.keys)
|
|
||||||
else
|
|
||||||
update_without_dirty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def field_changed?(attr, old, value)
|
|
||||||
if column = column_for_attribute(attr)
|
|
||||||
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
|
|
||||||
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
|
||||||
# Hence we don't record it as a change if the value changes from nil to ''.
|
|
||||||
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
|
||||||
# be typecast back to 0 (''.to_i => 0)
|
|
||||||
value = nil
|
|
||||||
else
|
|
||||||
value = column.type_cast(value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
old != value
|
|
||||||
end
|
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
def self.extended(base)
|
|
||||||
class << base
|
|
||||||
alias_method_chain :alias_attribute, :dirty
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def alias_attribute_with_dirty(new_name, old_name)
|
|
||||||
alias_attribute_without_dirty(new_name, old_name)
|
|
||||||
DIRTY_SUFFIXES.each do |suffix|
|
|
||||||
module_eval <<-STR, __FILE__, __LINE__+1
|
|
||||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
|
||||||
STR
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in a new issue