activerecord 3.0.0.beta3 → 3.0.0.beta4
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +27 -0
- data/lib/active_record.rb +5 -1
- data/lib/active_record/aggregations.rb +1 -0
- data/lib/active_record/association_preload.rb +1 -1
- data/lib/active_record/associations.rb +88 -33
- data/lib/active_record/associations/association_collection.rb +12 -11
- data/lib/active_record/associations/association_proxy.rb +1 -1
- data/lib/active_record/attribute_methods.rb +10 -1
- data/lib/active_record/attribute_methods/dirty.rb +50 -50
- data/lib/active_record/attribute_methods/primary_key.rb +1 -1
- data/lib/active_record/autosave_association.rb +20 -5
- data/lib/active_record/base.rb +28 -343
- data/lib/active_record/callbacks.rb +23 -34
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +1 -1
- data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +56 -0
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +11 -18
- data/lib/active_record/connection_adapters/abstract/quoting.rb +3 -7
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +3 -3
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +61 -7
- data/lib/active_record/connection_adapters/abstract_adapter.rb +3 -1
- data/lib/active_record/connection_adapters/mysql_adapter.rb +22 -50
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +31 -143
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +6 -2
- data/lib/active_record/counter_cache.rb +105 -0
- data/lib/active_record/fixtures.rb +16 -15
- data/lib/active_record/locale/en.yml +2 -2
- data/lib/active_record/locking/optimistic.rb +37 -16
- data/lib/active_record/migration.rb +7 -3
- data/lib/active_record/named_scope.rb +1 -5
- data/lib/active_record/nested_attributes.rb +13 -2
- data/lib/active_record/observer.rb +19 -7
- data/lib/active_record/persistence.rb +230 -0
- data/lib/active_record/railtie.rb +17 -23
- data/lib/active_record/railties/databases.rake +3 -2
- data/lib/active_record/relation.rb +5 -2
- data/lib/active_record/relation/batches.rb +6 -1
- data/lib/active_record/relation/calculations.rb +12 -9
- data/lib/active_record/relation/finder_methods.rb +14 -10
- data/lib/active_record/relation/query_methods.rb +62 -44
- data/lib/active_record/relation/spawn_methods.rb +6 -1
- data/lib/active_record/schema_dumper.rb +3 -0
- data/lib/active_record/serializers/xml_serializer.rb +30 -56
- data/lib/active_record/session_store.rb +1 -1
- data/lib/active_record/timestamp.rb +22 -26
- data/lib/active_record/transactions.rb +168 -50
- data/lib/active_record/validations.rb +33 -49
- data/lib/active_record/version.rb +1 -1
- data/lib/rails/generators/active_record.rb +6 -9
- metadata +27 -10
data/CHANGELOG
CHANGED
@@ -1,3 +1,30 @@
|
|
1
|
+
*Rails 3.0.0 [beta 4] (June 8th, 2010)*
|
2
|
+
|
3
|
+
* Fixed that ActiveRecord::Base.compute_type would swallow NoMethodError #4751 [Andrew Bloomgarden, Andrew White]
|
4
|
+
|
5
|
+
* Add index length support for MySQL. #1852 [Emili Parreno, Pratik Naik]
|
6
|
+
|
7
|
+
Example:
|
8
|
+
|
9
|
+
add_index(:accounts, :name, :name => 'by_name', :length => 10)
|
10
|
+
=> CREATE INDEX by_name ON accounts(name(10))
|
11
|
+
|
12
|
+
add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15})
|
13
|
+
=> CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
|
14
|
+
|
15
|
+
* find_or_create_by_attr(value, ...) works when attr is protected. #4457 [Santiago Pastorino, Marc-André Lafortune]
|
16
|
+
|
17
|
+
* New callbacks: after_commit and after_rollback. Do expensive operations like image thumbnailing after_commit instead of after_save. #2991 [Brian Durand]
|
18
|
+
|
19
|
+
* Serialized attributes are not converted to YAML if they are any of the formats that can be serialized to XML (like Hash, Array and Strings). [José Valim]
|
20
|
+
|
21
|
+
* Destroy uses optimistic locking. If lock_version on the record you're destroying doesn't match lock_version in the database, a StaleObjectError is raised. #1966 [Curtis Hawthorne]
|
22
|
+
|
23
|
+
* PostgreSQL: drop support for old postgres driver. Use pg 0.9.0 or later. [Jeremy Kemper]
|
24
|
+
|
25
|
+
* Observers can prevent records from saving by returning false, just like before_save and friends. #4087 [Mislav Marohnić]
|
26
|
+
|
27
|
+
|
1
28
|
*Rails 3.0.0 [beta 3] (April 13th, 2010)*
|
2
29
|
|
3
30
|
* Add Relation extensions. [Pratik Naik]
|
data/lib/active_record.rb
CHANGED
@@ -29,7 +29,9 @@ activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__)
|
|
29
29
|
$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path)
|
30
30
|
|
31
31
|
require 'active_support'
|
32
|
+
require 'active_support/i18n'
|
32
33
|
require 'active_model'
|
34
|
+
require 'arel'
|
33
35
|
|
34
36
|
module ActiveRecord
|
35
37
|
extend ActiveSupport::Autoload
|
@@ -59,6 +61,7 @@ module ActiveRecord
|
|
59
61
|
|
60
62
|
autoload :Base
|
61
63
|
autoload :Callbacks
|
64
|
+
autoload :CounterCache
|
62
65
|
autoload :DynamicFinderMatch
|
63
66
|
autoload :DynamicScopeMatch
|
64
67
|
autoload :Migration
|
@@ -66,6 +69,7 @@ module ActiveRecord
|
|
66
69
|
autoload :NamedScope
|
67
70
|
autoload :NestedAttributes
|
68
71
|
autoload :Observer
|
72
|
+
autoload :Persistence
|
69
73
|
autoload :QueryCache
|
70
74
|
autoload :Reflection
|
71
75
|
autoload :Schema
|
@@ -117,4 +121,4 @@ ActiveSupport.on_load(:active_record) do
|
|
117
121
|
Arel::Table.engine = Arel::Sql::Engine.new(self)
|
118
122
|
end
|
119
123
|
|
120
|
-
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
|
124
|
+
I18n.load_path << File.dirname(__FILE__) + '/active_record/locale/en.yml'
|
@@ -253,6 +253,7 @@ module ActiveRecord
|
|
253
253
|
raise ArgumentError, 'Converter must be a symbol denoting the converter method to call or a Proc to be invoked.'
|
254
254
|
end
|
255
255
|
end
|
256
|
+
|
256
257
|
mapping.each { |pair| self[pair.first] = part.send(pair.last) }
|
257
258
|
instance_variable_set("@#{name}", part.freeze)
|
258
259
|
end
|
@@ -2,6 +2,7 @@ require 'active_support/core_ext/array/wrap'
|
|
2
2
|
require 'active_support/core_ext/enumerable'
|
3
3
|
require 'active_support/core_ext/module/delegation'
|
4
4
|
require 'active_support/core_ext/object/blank'
|
5
|
+
require 'active_support/core_ext/string/conversions'
|
5
6
|
|
6
7
|
module ActiveRecord
|
7
8
|
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
|
@@ -839,10 +840,11 @@ module ActiveRecord
|
|
839
840
|
# If set to <tt>:destroy</tt> all the associated objects are destroyed
|
840
841
|
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
|
841
842
|
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
|
842
|
-
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks.
|
843
|
-
#
|
844
|
-
#
|
845
|
-
#
|
843
|
+
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. If set to
|
844
|
+
# <tt>:restrict</tt> this object cannot be deleted if it has any associated object.
|
845
|
+
#
|
846
|
+
# *Warning:* This option is ignored when used with <tt>:through</tt> option.
|
847
|
+
#
|
846
848
|
# [:finder_sql]
|
847
849
|
# Specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
848
850
|
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
|
@@ -1304,14 +1306,14 @@ module ActiveRecord
|
|
1304
1306
|
|
1305
1307
|
# Don't use a before_destroy callback since users' before_destroy
|
1306
1308
|
# callbacks will be executed after the association is wiped out.
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
#
|
1313
|
-
|
1314
|
-
|
1309
|
+
include Module.new {
|
1310
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
1311
|
+
def destroy # def destroy
|
1312
|
+
super # super
|
1313
|
+
#{reflection.name}.clear # posts.clear
|
1314
|
+
end # end
|
1315
|
+
RUBY
|
1316
|
+
}
|
1315
1317
|
|
1316
1318
|
add_association_callbacks(reflection.name, options)
|
1317
1319
|
end
|
@@ -1398,7 +1400,7 @@ module ActiveRecord
|
|
1398
1400
|
primary_key = reflection.source_reflection.primary_key_name
|
1399
1401
|
send(through.name).select("DISTINCT #{through.quoted_table_name}.#{primary_key}").map!(&:"#{primary_key}")
|
1400
1402
|
else
|
1401
|
-
send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").map!(&:id)
|
1403
|
+
send(reflection.name).select("#{reflection.quoted_table_name}.#{reflection.klass.primary_key}").except(:includes).map!(&:id)
|
1402
1404
|
end
|
1403
1405
|
end
|
1404
1406
|
end
|
@@ -1460,7 +1462,7 @@ module ActiveRecord
|
|
1460
1462
|
before_destroy(method_name)
|
1461
1463
|
|
1462
1464
|
module_eval(
|
1463
|
-
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
|
1465
|
+
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)", __FILE__, __LINE__
|
1464
1466
|
)
|
1465
1467
|
end
|
1466
1468
|
|
@@ -1500,7 +1502,16 @@ module ActiveRecord
|
|
1500
1502
|
when :destroy
|
1501
1503
|
method_name = "has_many_dependent_destroy_for_#{reflection.name}".to_sym
|
1502
1504
|
define_method(method_name) do
|
1503
|
-
send(reflection.name).each
|
1505
|
+
send(reflection.name).each do |o|
|
1506
|
+
# No point in executing the counter update since we're going to destroy the parent anyway
|
1507
|
+
counter_method = ('belongs_to_counter_cache_before_destroy_for_' + self.class.name.downcase).to_sym
|
1508
|
+
if(o.respond_to? counter_method) then
|
1509
|
+
class << o
|
1510
|
+
self
|
1511
|
+
end.send(:define_method, counter_method, Proc.new {})
|
1512
|
+
end
|
1513
|
+
o.destroy
|
1514
|
+
end
|
1504
1515
|
end
|
1505
1516
|
before_destroy method_name
|
1506
1517
|
when :delete_all
|
@@ -1728,6 +1739,14 @@ module ActiveRecord
|
|
1728
1739
|
build(associations)
|
1729
1740
|
end
|
1730
1741
|
|
1742
|
+
def graft(*associations)
|
1743
|
+
associations.each do |association|
|
1744
|
+
join_associations.detect {|a| association == a} ||
|
1745
|
+
build(association.reflection.name, association.find_parent_in(self), association.join_class)
|
1746
|
+
end
|
1747
|
+
self
|
1748
|
+
end
|
1749
|
+
|
1731
1750
|
def join_associations
|
1732
1751
|
@joins[1..-1].to_a
|
1733
1752
|
end
|
@@ -1736,6 +1755,17 @@ module ActiveRecord
|
|
1736
1755
|
@joins[0]
|
1737
1756
|
end
|
1738
1757
|
|
1758
|
+
def count_aliases_from_table_joins(name)
|
1759
|
+
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
|
1760
|
+
quoted_name = join_base.active_record.connection.quote_table_name(name.downcase).downcase
|
1761
|
+
join_sql = join_base.table_joins.to_s.downcase
|
1762
|
+
join_sql.blank? ? 0 :
|
1763
|
+
# Table names
|
1764
|
+
join_sql.scan(/join(?:\s+\w+)?\s+#{quoted_name}\son/).size +
|
1765
|
+
# Table aliases
|
1766
|
+
join_sql.scan(/join(?:\s+\w+)?\s+\S+\s+#{quoted_name}\son/).size
|
1767
|
+
end
|
1768
|
+
|
1739
1769
|
def instantiate(rows)
|
1740
1770
|
rows.each_with_index do |row, i|
|
1741
1771
|
primary_id = join_base.record_id(row)
|
@@ -1780,22 +1810,22 @@ module ActiveRecord
|
|
1780
1810
|
end
|
1781
1811
|
|
1782
1812
|
protected
|
1783
|
-
def build(associations, parent = nil)
|
1813
|
+
def build(associations, parent = nil, join_class = Arel::InnerJoin)
|
1784
1814
|
parent ||= @joins.last
|
1785
1815
|
case associations
|
1786
1816
|
when Symbol, String
|
1787
1817
|
reflection = parent.reflections[associations.to_s.intern] or
|
1788
1818
|
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
|
1789
1819
|
@reflections << reflection
|
1790
|
-
@joins << build_join_association(reflection, parent)
|
1820
|
+
@joins << build_join_association(reflection, parent).with_join_class(join_class)
|
1791
1821
|
when Array
|
1792
1822
|
associations.each do |association|
|
1793
|
-
build(association, parent)
|
1823
|
+
build(association, parent, join_class)
|
1794
1824
|
end
|
1795
1825
|
when Hash
|
1796
1826
|
associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
|
1797
|
-
build(name, parent)
|
1798
|
-
build(associations[name])
|
1827
|
+
build(name, parent, join_class)
|
1828
|
+
build(associations[name], nil, join_class)
|
1799
1829
|
end
|
1800
1830
|
else
|
1801
1831
|
raise ConfigurationError, associations.inspect
|
@@ -1872,6 +1902,12 @@ module ActiveRecord
|
|
1872
1902
|
@table_joins = joins
|
1873
1903
|
end
|
1874
1904
|
|
1905
|
+
def ==(other)
|
1906
|
+
other.class == self.class &&
|
1907
|
+
other.active_record == active_record &&
|
1908
|
+
other.table_joins == table_joins
|
1909
|
+
end
|
1910
|
+
|
1875
1911
|
def aliased_prefix
|
1876
1912
|
"t0"
|
1877
1913
|
end
|
@@ -1937,6 +1973,27 @@ module ActiveRecord
|
|
1937
1973
|
end
|
1938
1974
|
end
|
1939
1975
|
|
1976
|
+
def ==(other)
|
1977
|
+
other.class == self.class &&
|
1978
|
+
other.reflection == reflection &&
|
1979
|
+
other.parent == parent
|
1980
|
+
end
|
1981
|
+
|
1982
|
+
def find_parent_in(other_join_dependency)
|
1983
|
+
other_join_dependency.joins.detect do |join|
|
1984
|
+
self.parent == join
|
1985
|
+
end
|
1986
|
+
end
|
1987
|
+
|
1988
|
+
def join_class
|
1989
|
+
@join_class ||= Arel::InnerJoin
|
1990
|
+
end
|
1991
|
+
|
1992
|
+
def with_join_class(join_class)
|
1993
|
+
@join_class = join_class
|
1994
|
+
self
|
1995
|
+
end
|
1996
|
+
|
1940
1997
|
def association_join
|
1941
1998
|
return @join if @join
|
1942
1999
|
|
@@ -2036,27 +2093,25 @@ module ActiveRecord
|
|
2036
2093
|
end
|
2037
2094
|
|
2038
2095
|
def join_relation(joining_relation, join = nil)
|
2039
|
-
|
2040
|
-
joining_relation.joins(Relation::JoinOperation.new(relations.first, Arel::OuterJoin, association_join.first)).
|
2041
|
-
joins(Relation::JoinOperation.new(relations.last, Arel::OuterJoin, association_join.last))
|
2042
|
-
else
|
2043
|
-
joining_relation.joins(Relation::JoinOperation.new(relations, Arel::OuterJoin, association_join))
|
2044
|
-
end
|
2096
|
+
joining_relation.joins(self.with_join_class(Arel::OuterJoin))
|
2045
2097
|
end
|
2046
2098
|
|
2047
2099
|
protected
|
2048
2100
|
|
2049
2101
|
def aliased_table_name_for(name, suffix = nil)
|
2050
|
-
if
|
2051
|
-
@join_dependency.table_aliases[name]
|
2102
|
+
if @join_dependency.table_aliases[name].zero?
|
2103
|
+
@join_dependency.table_aliases[name] = @join_dependency.count_aliases_from_table_joins(name)
|
2052
2104
|
end
|
2053
2105
|
|
2054
|
-
|
2055
|
-
# if the table name has been used, then use an alias
|
2106
|
+
if !@join_dependency.table_aliases[name].zero? # We need an alias
|
2056
2107
|
name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
|
2057
|
-
table_index = @join_dependency.table_aliases[name]
|
2058
2108
|
@join_dependency.table_aliases[name] += 1
|
2059
|
-
|
2109
|
+
if @join_dependency.table_aliases[name] == 1 # First time we've seen this name
|
2110
|
+
# Also need to count the aliases from the table_aliases to avoid incorrect count
|
2111
|
+
@join_dependency.table_aliases[name] += @join_dependency.count_aliases_from_table_joins(name)
|
2112
|
+
end
|
2113
|
+
table_index = @join_dependency.table_aliases[name]
|
2114
|
+
name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index}" if table_index > 1
|
2060
2115
|
else
|
2061
2116
|
@join_dependency.table_aliases[name] += 1
|
2062
2117
|
end
|
@@ -2077,7 +2132,7 @@ module ActiveRecord
|
|
2077
2132
|
end
|
2078
2133
|
|
2079
2134
|
def interpolate_sql(sql)
|
2080
|
-
instance_eval("%@#{sql.gsub('@', '\@')}@")
|
2135
|
+
instance_eval("%@#{sql.gsub('@', '\@')}@", __FILE__, __LINE__)
|
2081
2136
|
end
|
2082
2137
|
end
|
2083
2138
|
end
|
@@ -411,7 +411,8 @@ module ActiveRecord
|
|
411
411
|
end
|
412
412
|
elsif @reflection.klass.scopes[method]
|
413
413
|
@_named_scopes_cache ||= {}
|
414
|
-
@_named_scopes_cache[method] ||=
|
414
|
+
@_named_scopes_cache[method] ||= {}
|
415
|
+
@_named_scopes_cache[method][args] ||= with_scope(construct_scope) { @reflection.klass.send(method, *args) }
|
415
416
|
else
|
416
417
|
with_scope(construct_scope) do
|
417
418
|
if block_given?
|
@@ -451,6 +452,16 @@ module ActiveRecord
|
|
451
452
|
records
|
452
453
|
end
|
453
454
|
|
455
|
+
def add_record_to_target_with_callbacks(record)
|
456
|
+
callback(:before_add, record)
|
457
|
+
yield(record) if block_given?
|
458
|
+
@target ||= [] unless loaded?
|
459
|
+
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
460
|
+
callback(:after_add, record)
|
461
|
+
set_inverse_instance(record, @owner)
|
462
|
+
record
|
463
|
+
end
|
464
|
+
|
454
465
|
private
|
455
466
|
def create_record(attrs)
|
456
467
|
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
@@ -475,16 +486,6 @@ module ActiveRecord
|
|
475
486
|
end
|
476
487
|
end
|
477
488
|
|
478
|
-
def add_record_to_target_with_callbacks(record)
|
479
|
-
callback(:before_add, record)
|
480
|
-
yield(record) if block_given?
|
481
|
-
@target ||= [] unless loaded?
|
482
|
-
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
483
|
-
callback(:after_add, record)
|
484
|
-
set_inverse_instance(record, @owner)
|
485
|
-
record
|
486
|
-
end
|
487
|
-
|
488
489
|
def remove_records(*records)
|
489
490
|
records = flatten_deeper(records)
|
490
491
|
records.each { |record| raise_on_type_mismatch(record) }
|
@@ -51,7 +51,7 @@ module ActiveRecord
|
|
51
51
|
alias_method :proxy_respond_to?, :respond_to?
|
52
52
|
alias_method :proxy_extend, :extend
|
53
53
|
delegate :to_param, :to => :proxy_target
|
54
|
-
instance_methods.each { |m| undef_method m unless m =~ /^(?:nil\?|send|object_id|to_a)$|^__|proxy_/ }
|
54
|
+
instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|proxy_/ }
|
55
55
|
|
56
56
|
def initialize(owner, reflection)
|
57
57
|
@owner, @reflection = owner, reflection
|
@@ -18,10 +18,19 @@ module ActiveRecord
|
|
18
18
|
def instance_method_already_implemented?(method_name)
|
19
19
|
method_name = method_name.to_s
|
20
20
|
@_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
|
21
|
-
@@_defined_activerecord_methods ||=
|
21
|
+
@@_defined_activerecord_methods ||= defined_activerecord_methods
|
22
22
|
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
|
23
23
|
@_defined_class_methods.include?(method_name)
|
24
24
|
end
|
25
|
+
|
26
|
+
def defined_activerecord_methods
|
27
|
+
active_record = ActiveRecord::Base
|
28
|
+
super_klass = ActiveRecord::Base.superclass
|
29
|
+
methods = active_record.public_instance_methods - super_klass.public_instance_methods
|
30
|
+
methods += active_record.private_instance_methods - super_klass.private_instance_methods
|
31
|
+
methods += active_record.protected_instance_methods - super_klass.protected_instance_methods
|
32
|
+
methods.map {|m| m.to_s }.to_set
|
33
|
+
end
|
25
34
|
end
|
26
35
|
|
27
36
|
def method_missing(method_id, *args, &block)
|
@@ -5,20 +5,20 @@ module ActiveRecord
|
|
5
5
|
module Dirty
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
include ActiveModel::Dirty
|
8
|
+
include AttributeMethods::Write
|
8
9
|
|
9
10
|
included do
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
alias_method_chain :reload, :dirty
|
11
|
+
if self < ::ActiveRecord::Timestamp
|
12
|
+
raise "You cannot include Dirty after Timestamp"
|
13
|
+
end
|
14
14
|
|
15
15
|
superclass_delegating_accessor :partial_updates
|
16
16
|
self.partial_updates = true
|
17
17
|
end
|
18
18
|
|
19
19
|
# Attempts to +save+ the record and clears changed attributes if successful.
|
20
|
-
def
|
21
|
-
if status =
|
20
|
+
def save(*) #:nodoc:
|
21
|
+
if status = super
|
22
22
|
@previously_changed = changes
|
23
23
|
@changed_attributes.clear
|
24
24
|
end
|
@@ -26,70 +26,70 @@ module ActiveRecord
|
|
26
26
|
end
|
27
27
|
|
28
28
|
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
29
|
-
def
|
30
|
-
|
29
|
+
def save!(*) #:nodoc:
|
30
|
+
super.tap do
|
31
31
|
@previously_changed = changes
|
32
32
|
@changed_attributes.clear
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
36
|
# <tt>reload</tt> the record and clears changed attributes.
|
37
|
-
def
|
38
|
-
|
37
|
+
def reload(*) #:nodoc:
|
38
|
+
super.tap do
|
39
39
|
@previously_changed.clear
|
40
40
|
@changed_attributes.clear
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
private
|
45
|
+
# Wrap write_attribute to remember original attribute value.
|
46
|
+
def write_attribute(attr, value)
|
47
|
+
attr = attr.to_s
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
49
|
+
# The attribute already has an unsaved change.
|
50
|
+
if attribute_changed?(attr)
|
51
|
+
old = @changed_attributes[attr]
|
52
|
+
@changed_attributes.delete(attr) unless field_changed?(attr, old, value)
|
53
|
+
else
|
54
|
+
old = clone_attribute_value(:read_attribute, attr)
|
55
|
+
# Save Time objects as TimeWithZone if time_zone_aware_attributes == true
|
56
|
+
old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old)
|
57
|
+
@changed_attributes[attr] = old if field_changed?(attr, old, value)
|
58
|
+
end
|
59
59
|
|
60
|
-
|
61
|
-
|
60
|
+
# Carry on.
|
61
|
+
super(attr, value)
|
62
|
+
end
|
63
|
+
|
64
|
+
def update(*)
|
65
|
+
if partial_updates?
|
66
|
+
# Serialized attributes should always be written in case they've been
|
67
|
+
# changed in place.
|
68
|
+
super(changed | (attributes.keys & self.class.serialized_attributes.keys))
|
69
|
+
else
|
70
|
+
super
|
62
71
|
end
|
72
|
+
end
|
63
73
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
#
|
68
|
-
|
74
|
+
def field_changed?(attr, old, value)
|
75
|
+
if column = column_for_attribute(attr)
|
76
|
+
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
|
77
|
+
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
78
|
+
# Hence we don't record it as a change if the value changes from nil to ''.
|
79
|
+
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
80
|
+
# be typecast back to 0 (''.to_i => 0)
|
81
|
+
value = nil
|
69
82
|
else
|
70
|
-
|
83
|
+
value = column.type_cast(value)
|
71
84
|
end
|
72
85
|
end
|
73
86
|
|
74
|
-
|
75
|
-
|
76
|
-
if column.number? && column.null && (old.nil? || old == 0) && value.blank?
|
77
|
-
# For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
|
78
|
-
# Hence we don't record it as a change if the value changes from nil to ''.
|
79
|
-
# If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
|
80
|
-
# be typecast back to 0 (''.to_i => 0)
|
81
|
-
value = nil
|
82
|
-
else
|
83
|
-
value = column.type_cast(value)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
old != value
|
88
|
-
end
|
87
|
+
old != value
|
88
|
+
end
|
89
89
|
|
90
|
-
|
91
|
-
|
92
|
-
|
90
|
+
def clone_with_time_zone_conversion_attribute?(attr, old)
|
91
|
+
old.class.name == "Time" && time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(attr.to_sym)
|
92
|
+
end
|
93
93
|
end
|
94
94
|
end
|
95
95
|
end
|