activerecord 2.2.3 → 2.3.2

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.

Files changed (120) hide show
  1. data/CHANGELOG +438 -396
  2. data/Rakefile +4 -2
  3. data/lib/active_record.rb +46 -43
  4. data/lib/active_record/association_preload.rb +34 -19
  5. data/lib/active_record/associations.rb +193 -251
  6. data/lib/active_record/associations/association_collection.rb +38 -21
  7. data/lib/active_record/associations/association_proxy.rb +11 -4
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +2 -2
  9. data/lib/active_record/associations/has_many_association.rb +2 -2
  10. data/lib/active_record/associations/has_many_through_association.rb +8 -8
  11. data/lib/active_record/associations/has_one_association.rb +11 -2
  12. data/lib/active_record/attribute_methods.rb +1 -0
  13. data/lib/active_record/autosave_association.rb +349 -0
  14. data/lib/active_record/base.rb +292 -106
  15. data/lib/active_record/batches.rb +73 -0
  16. data/lib/active_record/calculations.rb +34 -16
  17. data/lib/active_record/callbacks.rb +37 -8
  18. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +16 -0
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +3 -0
  20. data/lib/active_record/connection_adapters/abstract/database_statements.rb +103 -15
  21. data/lib/active_record/connection_adapters/abstract/query_cache.rb +6 -6
  22. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +28 -25
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -5
  24. data/lib/active_record/connection_adapters/mysql_adapter.rb +50 -21
  25. data/lib/active_record/connection_adapters/postgresql_adapter.rb +26 -41
  26. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +1 -1
  27. data/lib/active_record/connection_adapters/sqlite_adapter.rb +41 -21
  28. data/lib/active_record/dirty.rb +1 -1
  29. data/lib/active_record/dynamic_scope_match.rb +25 -0
  30. data/lib/active_record/fixtures.rb +193 -198
  31. data/lib/active_record/locale/en.yml +1 -1
  32. data/lib/active_record/locking/optimistic.rb +33 -0
  33. data/lib/active_record/migration.rb +8 -2
  34. data/lib/active_record/named_scope.rb +13 -6
  35. data/lib/active_record/nested_attributes.rb +329 -0
  36. data/lib/active_record/query_cache.rb +25 -13
  37. data/lib/active_record/reflection.rb +6 -1
  38. data/lib/active_record/schema_dumper.rb +2 -0
  39. data/lib/active_record/serialization.rb +3 -1
  40. data/lib/active_record/serializers/json_serializer.rb +19 -0
  41. data/lib/active_record/serializers/xml_serializer.rb +28 -13
  42. data/lib/active_record/session_store.rb +318 -0
  43. data/lib/active_record/test_case.rb +15 -9
  44. data/lib/active_record/timestamp.rb +2 -2
  45. data/lib/active_record/transactions.rb +58 -8
  46. data/lib/active_record/validations.rb +29 -24
  47. data/lib/active_record/version.rb +2 -2
  48. data/test/cases/ar_schema_test.rb +0 -1
  49. data/test/cases/associations/belongs_to_associations_test.rb +35 -131
  50. data/test/cases/associations/cascaded_eager_loading_test.rb +8 -0
  51. data/test/cases/associations/eager_load_nested_include_test.rb +29 -0
  52. data/test/cases/associations/eager_test.rb +137 -7
  53. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +45 -7
  54. data/test/cases/associations/has_many_associations_test.rb +110 -149
  55. data/test/cases/associations/has_many_through_associations_test.rb +39 -7
  56. data/test/cases/associations/has_one_associations_test.rb +39 -92
  57. data/test/cases/associations/has_one_through_associations_test.rb +34 -3
  58. data/test/cases/associations/inner_join_association_test.rb +0 -5
  59. data/test/cases/associations/join_model_test.rb +5 -7
  60. data/test/cases/attribute_methods_test.rb +13 -1
  61. data/test/cases/autosave_association_test.rb +901 -0
  62. data/test/cases/base_test.rb +41 -21
  63. data/test/cases/batches_test.rb +61 -0
  64. data/test/cases/calculations_test.rb +37 -17
  65. data/test/cases/callbacks_test.rb +43 -5
  66. data/test/cases/connection_pool_test.rb +25 -0
  67. data/test/cases/copy_table_test_sqlite.rb +11 -0
  68. data/test/cases/datatype_test_postgresql.rb +1 -0
  69. data/test/cases/defaults_test.rb +37 -26
  70. data/test/cases/dirty_test.rb +26 -2
  71. data/test/cases/finder_test.rb +79 -44
  72. data/test/cases/fixtures_test.rb +15 -19
  73. data/test/cases/helper.rb +26 -19
  74. data/test/cases/inheritance_test.rb +2 -2
  75. data/test/cases/json_serialization_test.rb +1 -1
  76. data/test/cases/locking_test.rb +23 -5
  77. data/test/cases/method_scoping_test.rb +126 -3
  78. data/test/cases/migration_test.rb +253 -237
  79. data/test/cases/named_scope_test.rb +73 -3
  80. data/test/cases/nested_attributes_test.rb +509 -0
  81. data/test/cases/query_cache_test.rb +0 -4
  82. data/test/cases/reflection_test.rb +13 -3
  83. data/test/cases/reload_models_test.rb +3 -1
  84. data/test/cases/repair_helper.rb +50 -0
  85. data/test/cases/schema_dumper_test.rb +0 -1
  86. data/test/cases/transactions_test.rb +177 -12
  87. data/test/cases/validations_i18n_test.rb +288 -294
  88. data/test/cases/validations_test.rb +230 -180
  89. data/test/cases/xml_serialization_test.rb +19 -1
  90. data/test/fixtures/fixture_database.sqlite3 +0 -0
  91. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  92. data/test/fixtures/member_types.yml +6 -0
  93. data/test/fixtures/members.yml +3 -1
  94. data/test/fixtures/people.yml +10 -1
  95. data/test/fixtures/toys.yml +4 -0
  96. data/test/models/author.rb +1 -2
  97. data/test/models/bird.rb +3 -0
  98. data/test/models/category.rb +1 -0
  99. data/test/models/company.rb +3 -0
  100. data/test/models/developer.rb +12 -0
  101. data/test/models/event.rb +3 -0
  102. data/test/models/member.rb +1 -0
  103. data/test/models/member_detail.rb +1 -0
  104. data/test/models/member_type.rb +3 -0
  105. data/test/models/owner.rb +2 -1
  106. data/test/models/parrot.rb +2 -0
  107. data/test/models/person.rb +6 -0
  108. data/test/models/pet.rb +2 -1
  109. data/test/models/pirate.rb +55 -1
  110. data/test/models/post.rb +6 -0
  111. data/test/models/project.rb +1 -0
  112. data/test/models/reply.rb +6 -0
  113. data/test/models/ship.rb +8 -1
  114. data/test/models/ship_part.rb +5 -0
  115. data/test/models/topic.rb +13 -1
  116. data/test/models/toy.rb +4 -0
  117. data/test/schema/schema.rb +35 -2
  118. metadata +70 -9
  119. data/test/fixtures/fixture_database.sqlite +0 -0
  120. data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -60,7 +60,7 @@ module ActiveRecord
60
60
  @reflection.klass.find(*args)
61
61
  end
62
62
  end
63
-
63
+
64
64
  # Fetches the first one using SQL if possible.
65
65
  def first(*args)
66
66
  if fetch_first_or_last_using_find?(args)
@@ -83,7 +83,11 @@ module ActiveRecord
83
83
 
84
84
  def to_ary
85
85
  load_target
86
- @target.to_ary
86
+ if @target.is_a?(Array)
87
+ @target.to_ary
88
+ else
89
+ Array(@target)
90
+ end
87
91
  end
88
92
 
89
93
  def reset
@@ -182,7 +186,6 @@ module ActiveRecord
182
186
  end
183
187
  end
184
188
 
185
-
186
189
  # Removes +records+ from this association calling +before_remove+ and
187
190
  # +after_remove+ callbacks.
188
191
  #
@@ -191,20 +194,23 @@ module ActiveRecord
191
194
  # are actually removed from the database, that depends precisely on
192
195
  # +delete_records+. They are in any case removed from the collection.
193
196
  def delete(*records)
194
- records = flatten_deeper(records)
195
- records.each { |record| raise_on_type_mismatch(record) }
196
-
197
- transaction do
198
- records.each { |record| callback(:before_remove, record) }
199
-
200
- old_records = records.reject {|r| r.new_record? }
197
+ remove_records(records) do |records, old_records|
201
198
  delete_records(old_records) if old_records.any?
202
-
203
- records.each do |record|
204
- @target.delete(record)
205
- callback(:after_remove, record)
206
- end
199
+ records.each { |record| @target.delete(record) }
200
+ end
201
+ end
202
+
203
+ # Destroy +records+ and remove from this association calling +before_remove+
204
+ # and +after_remove+ callbacks.
205
+ #
206
+ # Note this method will always remove records from database ignoring the
207
+ # +:dependent+ option.
208
+ def destroy(*records)
209
+ remove_records(records) do |records, old_records|
210
+ old_records.each { |record| record.destroy }
207
211
  end
212
+
213
+ load_target
208
214
  end
209
215
 
210
216
  # Removes all records from this association. Returns +self+ so method calls may be chained.
@@ -219,15 +225,14 @@ module ActiveRecord
219
225
 
220
226
  self
221
227
  end
222
-
223
- def destroy_all
224
- transaction do
225
- each { |record| record.destroy }
226
- end
227
228
 
229
+ # Destory all the records from this association
230
+ def destroy_all
231
+ load_target
232
+ destroy(@target)
228
233
  reset_target!
229
234
  end
230
-
235
+
231
236
  def create(attrs = {})
232
237
  if attrs.is_a?(Array)
233
238
  attrs.collect { |attr| create(attr) }
@@ -427,6 +432,18 @@ module ActiveRecord
427
432
  record
428
433
  end
429
434
 
435
+ def remove_records(*records)
436
+ records = flatten_deeper(records)
437
+ records.each { |record| raise_on_type_mismatch(record) }
438
+
439
+ transaction do
440
+ records.each { |record| callback(:before_remove, record) }
441
+ old_records = records.reject { |r| r.new_record? }
442
+ yield(records, old_records)
443
+ records.each { |record| callback(:after_remove, record) }
444
+ end
445
+ end
446
+
430
447
  def callback(method, record)
431
448
  callbacks_for(method).each do |callback|
432
449
  ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
@@ -169,8 +169,8 @@ module ActiveRecord
169
169
  end
170
170
 
171
171
  # Forwards the call to the reflection class.
172
- def sanitize_sql(sql, table_name = @reflection.klass.quoted_table_name)
173
- @reflection.klass.send(:sanitize_sql, sql, table_name)
172
+ def sanitize_sql(sql)
173
+ @reflection.klass.send(:sanitize_sql, sql)
174
174
  end
175
175
 
176
176
  # Assigns the ID of the owner to the corresponding foreign key in +record+.
@@ -180,7 +180,10 @@ module ActiveRecord
180
180
  record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
181
181
  record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
182
182
  else
183
- record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
183
+ unless @owner.new_record?
184
+ primary_key = @reflection.options[:primary_key] || :id
185
+ record[@reflection.primary_key_name] = @owner.send(primary_key)
186
+ end
184
187
  end
185
188
  end
186
189
 
@@ -188,6 +191,7 @@ module ActiveRecord
188
191
  def merge_options_from_reflection!(options)
189
192
  options.reverse_merge!(
190
193
  :group => @reflection.options[:group],
194
+ :having => @reflection.options[:having],
191
195
  :limit => @reflection.options[:limit],
192
196
  :offset => @reflection.options[:offset],
193
197
  :joins => @reflection.options[:joins],
@@ -206,7 +210,10 @@ module ActiveRecord
206
210
  # Forwards any missing method call to the \target.
207
211
  def method_missing(method, *args)
208
212
  if load_target
209
- raise NoMethodError unless @target.respond_to?(method)
213
+ unless @target.respond_to?(method)
214
+ message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
215
+ raise NoMethodError, message
216
+ end
210
217
 
211
218
  if block_given?
212
219
  @target.send(method, *args) { |*block_args| yield(*block_args) }
@@ -28,12 +28,12 @@ module ActiveRecord
28
28
  load_target.size
29
29
  end
30
30
 
31
- def insert_record(record, force=true)
31
+ def insert_record(record, force = true, validate = true)
32
32
  if record.new_record?
33
33
  if force
34
34
  record.save!
35
35
  else
36
- return false unless record.save
36
+ return false unless record.save(validate)
37
37
  end
38
38
  end
39
39
 
@@ -56,9 +56,9 @@ module ActiveRecord
56
56
  "#{@reflection.name}_count"
57
57
  end
58
58
 
59
- def insert_record(record)
59
+ def insert_record(record, force = false, validate = true)
60
60
  set_belongs_to_association_for(record)
61
- record.save
61
+ force ? record.save! : record.save(validate)
62
62
  end
63
63
 
64
64
  # Deletes the records according to the <tt>:dependent</tt> option.
@@ -23,8 +23,8 @@ module ActiveRecord
23
23
  end
24
24
 
25
25
  # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
26
- # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
27
- # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
26
+ # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero,
27
+ # and you need to fetch that collection afterwards, it'll take one fewer SELECT query if you use #length.
28
28
  def size
29
29
  return @owner.send(:read_attribute, cached_counter_attribute_name) if has_cached_counter?
30
30
  return @target.size if loaded?
@@ -47,12 +47,12 @@ module ActiveRecord
47
47
  options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
48
48
  end
49
49
 
50
- def insert_record(record, force=true)
50
+ def insert_record(record, force = true, validate = true)
51
51
  if record.new_record?
52
52
  if force
53
53
  record.save!
54
54
  else
55
- return false unless record.save
55
+ return false unless record.save(validate)
56
56
  end
57
57
  end
58
58
  through_reflection = @reflection.through_reflection
@@ -150,7 +150,7 @@ module ActiveRecord
150
150
  end
151
151
  else
152
152
  reflection_primary_key = @reflection.source_reflection.primary_key_name
153
- source_primary_key = @reflection.klass.primary_key
153
+ source_primary_key = @reflection.through_reflection.klass.primary_key
154
154
  if @reflection.source_reflection.options[:as]
155
155
  polymorphic_join = "AND %s.%s = %s" % [
156
156
  @reflection.quoted_table_name, "#{@reflection.source_reflection.options[:as]}_type",
@@ -160,9 +160,9 @@ module ActiveRecord
160
160
  end
161
161
 
162
162
  "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
163
- @reflection.through_reflection.table_name,
164
- @reflection.table_name, reflection_primary_key,
165
- @reflection.through_reflection.table_name, source_primary_key,
163
+ @reflection.through_reflection.quoted_table_name,
164
+ @reflection.quoted_table_name, reflection_primary_key,
165
+ @reflection.through_reflection.quoted_table_name, source_primary_key,
166
166
  polymorphic_join
167
167
  ]
168
168
  end
@@ -29,8 +29,17 @@ module ActiveRecord
29
29
 
30
30
  unless @target.nil? || @target == obj
31
31
  if dependent? && !dont_save
32
- @target.destroy unless @target.new_record?
33
- @owner.clear_association_cache
32
+ case @reflection.options[:dependent]
33
+ when :delete
34
+ @target.delete unless @target.new_record?
35
+ @owner.clear_association_cache
36
+ when :destroy
37
+ @target.destroy unless @target.new_record?
38
+ @owner.clear_association_cache
39
+ when :nullify
40
+ @target[@reflection.primary_key_name] = nil
41
+ @target.save unless @owner.new_record? || @target.new_record?
42
+ end
34
43
  else
35
44
  @target[@reflection.primary_key_name] = nil
36
45
  @target.save unless @owner.new_record? || @target.new_record?
@@ -324,6 +324,7 @@ module ActiveRecord
324
324
  if Numeric === value || value !~ /[^0-9]/
325
325
  !value.to_i.zero?
326
326
  else
327
+ return false if ActiveRecord::ConnectionAdapters::Column::FALSE_VALUES.include?(value)
327
328
  !value.blank?
328
329
  end
329
330
  elsif column.number?
@@ -0,0 +1,349 @@
1
+ module ActiveRecord
2
+ # AutosaveAssociation is a module that takes care of automatically saving
3
+ # your associations when the parent is saved. In addition to saving, it
4
+ # also destroys any associations that were marked for destruction.
5
+ # (See mark_for_destruction and marked_for_destruction?)
6
+ #
7
+ # Saving of the parent, its associations, and the destruction of marked
8
+ # associations, all happen inside 1 transaction. This should never leave the
9
+ # database in an inconsistent state after, for instance, mass assigning
10
+ # attributes and saving them.
11
+ #
12
+ # If validations for any of the associations fail, their error messages will
13
+ # be applied to the parent.
14
+ #
15
+ # Note that it also means that associations marked for destruction won't
16
+ # be destroyed directly. They will however still be marked for destruction.
17
+ #
18
+ # === One-to-one Example
19
+ #
20
+ # Consider a Post model with one Author:
21
+ #
22
+ # class Post
23
+ # has_one :author, :autosave => true
24
+ # end
25
+ #
26
+ # Saving changes to the parent and its associated model can now be performed
27
+ # automatically _and_ atomically:
28
+ #
29
+ # post = Post.find(1)
30
+ # post.title # => "The current global position of migrating ducks"
31
+ # post.author.name # => "alloy"
32
+ #
33
+ # post.title = "On the migration of ducks"
34
+ # post.author.name = "Eloy Duran"
35
+ #
36
+ # post.save
37
+ # post.reload
38
+ # post.title # => "On the migration of ducks"
39
+ # post.author.name # => "Eloy Duran"
40
+ #
41
+ # Destroying an associated model, as part of the parent's save action, is as
42
+ # simple as marking it for destruction:
43
+ #
44
+ # post.author.mark_for_destruction
45
+ # post.author.marked_for_destruction? # => true
46
+ #
47
+ # Note that the model is _not_ yet removed from the database:
48
+ # id = post.author.id
49
+ # Author.find_by_id(id).nil? # => false
50
+ #
51
+ # post.save
52
+ # post.reload.author # => nil
53
+ #
54
+ # Now it _is_ removed from the database:
55
+ # Author.find_by_id(id).nil? # => true
56
+ #
57
+ # === One-to-many Example
58
+ #
59
+ # Consider a Post model with many Comments:
60
+ #
61
+ # class Post
62
+ # has_many :comments, :autosave => true
63
+ # end
64
+ #
65
+ # Saving changes to the parent and its associated model can now be performed
66
+ # automatically _and_ atomically:
67
+ #
68
+ # post = Post.find(1)
69
+ # post.title # => "The current global position of migrating ducks"
70
+ # post.comments.first.body # => "Wow, awesome info thanks!"
71
+ # post.comments.last.body # => "Actually, your article should be named differently."
72
+ #
73
+ # post.title = "On the migration of ducks"
74
+ # post.comments.last.body = "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
75
+ #
76
+ # post.save
77
+ # post.reload
78
+ # post.title # => "On the migration of ducks"
79
+ # post.comments.last.body # => "Actually, your article should be named differently. [UPDATED]: You are right, thanks."
80
+ #
81
+ # Destroying one of the associated models members, as part of the parent's
82
+ # save action, is as simple as marking it for destruction:
83
+ #
84
+ # post.comments.last.mark_for_destruction
85
+ # post.comments.last.marked_for_destruction? # => true
86
+ # post.comments.length # => 2
87
+ #
88
+ # Note that the model is _not_ yet removed from the database:
89
+ # id = post.comments.last.id
90
+ # Comment.find_by_id(id).nil? # => false
91
+ #
92
+ # post.save
93
+ # post.reload.comments.length # => 1
94
+ #
95
+ # Now it _is_ removed from the database:
96
+ # Comment.find_by_id(id).nil? # => true
97
+ #
98
+ # === Validation
99
+ #
100
+ # Validation is performed on the parent as usual, but also on all autosave
101
+ # enabled associations. If any of the associations fail validation, its
102
+ # error messages will be applied on the parents errors object and validation
103
+ # of the parent will fail.
104
+ #
105
+ # Consider a Post model with Author which validates the presence of its name
106
+ # attribute:
107
+ #
108
+ # class Post
109
+ # has_one :author, :autosave => true
110
+ # end
111
+ #
112
+ # class Author
113
+ # validates_presence_of :name
114
+ # end
115
+ #
116
+ # post = Post.find(1)
117
+ # post.author.name = ''
118
+ # post.save # => false
119
+ # post.errors # => #<ActiveRecord::Errors:0x174498c @errors={"author_name"=>["can't be blank"]}, @base=#<Post ...>>
120
+ #
121
+ # No validations will be performed on the associated models when validations
122
+ # are skipped for the parent:
123
+ #
124
+ # post = Post.find(1)
125
+ # post.author.name = ''
126
+ # post.save(false) # => true
127
+ module AutosaveAssociation
128
+ ASSOCIATION_TYPES = %w{ has_one belongs_to has_many has_and_belongs_to_many }
129
+
130
+ def self.included(base)
131
+ base.class_eval do
132
+ base.extend(ClassMethods)
133
+ alias_method_chain :reload, :autosave_associations
134
+
135
+ ASSOCIATION_TYPES.each do |type|
136
+ base.send("valid_keys_for_#{type}_association") << :autosave
137
+ end
138
+ end
139
+ end
140
+
141
+ module ClassMethods
142
+ private
143
+
144
+ # def belongs_to(name, options = {})
145
+ # super
146
+ # add_autosave_association_callbacks(reflect_on_association(name))
147
+ # end
148
+ ASSOCIATION_TYPES.each do |type|
149
+ module_eval %{
150
+ def #{type}(name, options = {})
151
+ super
152
+ add_autosave_association_callbacks(reflect_on_association(name))
153
+ end
154
+ }
155
+ end
156
+
157
+ # Adds a validate and save callback for the association as specified by
158
+ # the +reflection+.
159
+ def add_autosave_association_callbacks(reflection)
160
+ save_method = "autosave_associated_records_for_#{reflection.name}"
161
+ validation_method = "validate_associated_records_for_#{reflection.name}"
162
+ validate validation_method
163
+
164
+ case reflection.macro
165
+ when :has_many, :has_and_belongs_to_many
166
+ before_save :before_save_collection_association
167
+
168
+ define_method(save_method) { save_collection_association(reflection) }
169
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
170
+ after_create save_method
171
+ after_update save_method
172
+
173
+ define_method(validation_method) { validate_collection_association(reflection) }
174
+ else
175
+ case reflection.macro
176
+ when :has_one
177
+ define_method(save_method) { save_has_one_association(reflection) }
178
+ after_save save_method
179
+ when :belongs_to
180
+ define_method(save_method) { save_belongs_to_association(reflection) }
181
+ before_save save_method
182
+ end
183
+ define_method(validation_method) { validate_single_association(reflection) }
184
+ end
185
+ end
186
+ end
187
+
188
+ # Reloads the attributes of the object as usual and removes a mark for destruction.
189
+ def reload_with_autosave_associations(options = nil)
190
+ @marked_for_destruction = false
191
+ reload_without_autosave_associations(options)
192
+ end
193
+
194
+ # Marks this record to be destroyed as part of the parents save transaction.
195
+ # This does _not_ actually destroy the record yet, rather it will be destroyed when <tt>parent.save</tt> is called.
196
+ #
197
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
198
+ def mark_for_destruction
199
+ @marked_for_destruction = true
200
+ end
201
+
202
+ # Returns whether or not this record will be destroyed as part of the parents save transaction.
203
+ #
204
+ # Only useful if the <tt>:autosave</tt> option on the parent is enabled for this associated model.
205
+ def marked_for_destruction?
206
+ @marked_for_destruction
207
+ end
208
+
209
+ private
210
+
211
+ # Returns the record for an association collection that should be validated
212
+ # or saved. If +autosave+ is +false+ only new records will be returned,
213
+ # unless the parent is/was a new record itself.
214
+ def associated_records_to_validate_or_save(association, new_record, autosave)
215
+ if new_record
216
+ association
217
+ elsif association.loaded?
218
+ autosave ? association : association.select { |record| record.new_record? }
219
+ else
220
+ autosave ? association.target : association.target.select { |record| record.new_record? }
221
+ end
222
+ end
223
+
224
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
225
+ # turned on for the association specified by +reflection+.
226
+ def validate_single_association(reflection)
227
+ if reflection.options[:validate] == true || reflection.options[:autosave] == true
228
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
229
+ association_valid?(reflection, association)
230
+ end
231
+ end
232
+ end
233
+
234
+ # Validate the associated records if <tt>:validate</tt> or
235
+ # <tt>:autosave</tt> is turned on for the association specified by
236
+ # +reflection+.
237
+ def validate_collection_association(reflection)
238
+ if reflection.options[:validate] != false && association = association_instance_get(reflection.name)
239
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
240
+ records.each { |record| association_valid?(reflection, record) }
241
+ end
242
+ end
243
+ end
244
+
245
+ # Returns whether or not the association is valid and applies any errors to
246
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
247
+ # enabled records if they're marked_for_destruction?.
248
+ def association_valid?(reflection, association)
249
+ unless valid = association.valid?
250
+ if reflection.options[:autosave]
251
+ unless association.marked_for_destruction?
252
+ association.errors.each do |attribute, message|
253
+ attribute = "#{reflection.name}_#{attribute}"
254
+ errors.add(attribute, message) unless errors.on(attribute)
255
+ end
256
+ end
257
+ else
258
+ errors.add(reflection.name)
259
+ end
260
+ end
261
+ valid
262
+ end
263
+
264
+ # Is used as a before_save callback to check while saving a collection
265
+ # association whether or not the parent was a new record before saving.
266
+ def before_save_collection_association
267
+ @new_record_before_save = new_record?
268
+ true
269
+ end
270
+
271
+ # Saves any new associated records, or all loaded autosave associations if
272
+ # <tt>:autosave</tt> is enabled on the association.
273
+ #
274
+ # In addition, it destroys all children that were marked for destruction
275
+ # with mark_for_destruction.
276
+ #
277
+ # This all happens inside a transaction, _if_ the Transactions module is included into
278
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
279
+ def save_collection_association(reflection)
280
+ if association = association_instance_get(reflection.name)
281
+ autosave = reflection.options[:autosave]
282
+
283
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
284
+ records.each do |record|
285
+ if autosave && record.marked_for_destruction?
286
+ association.destroy(record)
287
+ elsif @new_record_before_save || record.new_record?
288
+ if autosave
289
+ association.send(:insert_record, record, false, false)
290
+ else
291
+ association.send(:insert_record, record)
292
+ end
293
+ elsif autosave
294
+ record.save(false)
295
+ end
296
+ end
297
+ end
298
+
299
+ # reconstruct the SQL queries now that we know the owner's id
300
+ association.send(:construct_sql) if association.respond_to?(:construct_sql)
301
+ end
302
+ end
303
+
304
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
305
+ # on the association.
306
+ #
307
+ # In addition, it will destroy the association if it was marked for
308
+ # destruction with mark_for_destruction.
309
+ #
310
+ # This all happens inside a transaction, _if_ the Transactions module is included into
311
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
312
+ def save_has_one_association(reflection)
313
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
314
+ if reflection.options[:autosave] && association.marked_for_destruction?
315
+ association.destroy
316
+ elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || reflection.options[:autosave]
317
+ association[reflection.primary_key_name] = id
318
+ association.save(false)
319
+ end
320
+ end
321
+ end
322
+
323
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
324
+ # on the association.
325
+ #
326
+ # In addition, it will destroy the association if it was marked for
327
+ # destruction with mark_for_destruction.
328
+ #
329
+ # This all happens inside a transaction, _if_ the Transactions module is included into
330
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
331
+ def save_belongs_to_association(reflection)
332
+ if association = association_instance_get(reflection.name)
333
+ if reflection.options[:autosave] && association.marked_for_destruction?
334
+ association.destroy
335
+ else
336
+ association.save(false) if association.new_record? || reflection.options[:autosave]
337
+
338
+ if association.updated?
339
+ self[reflection.primary_key_name] = association.id
340
+ # TODO: Removing this code doesn't seem to matter…
341
+ if reflection.options[:polymorphic]
342
+ self[reflection.options[:foreign_type]] = association.class.base_class.name.to_s
343
+ end
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end